2013년 12월 15일 일요일

android(armv7)에 node.js 올리기. (Rockchip RK3188/MK809C)

http://masashi-k.blogspot.kr/2013/08/nodejs-on-android.html
이쪽 글을 참조했다.

환경은 일단 RK3188 계열인 MK809C로 시도해보았다.

먼저 루팅이 되어있고 Linux Installer나 Debian Tool kit등으로 리눅스를 설치했고
터미널 클라이언트나 SSH를 통해 접근 가능한 상태라고 가정한다.

현재 node.js의 안정(stable) 버전은 v0.10.23이므로 해당 소스를 다운받자.
http://nodejs.org/download/
에 들어가서
http://nodejs.org/dist/v0.10.23/node-v0.10.23.tar.gz

직접 소스를 받으면 된다.

$ wget http://nodejs.org/dist/v0.10.23/node-v0.10.23.tar.gz
으로 받자.
$ tar xzf node-v0.10.23.tar.gz
$ cd node-v0.10.23
해서
빌드 준비를 하자.

$ ./configure --without-snapshot --dest-cpu=arm --dest-os=linux
로 먼저 빌드 환경을 잡고
바로 make 하기전에 thumb inter-working 관련 오류를 피하기 위해
macro-assembler-arm.cc 파일의 일부를 수정한다.
압축푼 경로를 기준으로 vi ./deps/v8/src/arm/macro-assembler-arm.cc 파일을 열어
60라인 근처를 수정하자.

#if defined(USE_THUMB_INTERWORK) && !defined(CAN_USE_THUMB_INSTRUCTIONS)
# error "For thumb inter-working we require an architecture which supports blx"
#endif

아마 이런 부분이 있을 것이다.
# error 부분 줄을 삭제하거나 // 처리 하자.
(http://www.yoovant.com/install-node-0-10-on-arm-based-board/ 참조)

$ make
꽤 오래 걸린다. 다른 일을 하다가 오자.

이상이 없다면
$ make install
하여 마무리 한다.


node -v, npm -v 로 v0.10.23 , 1.3.17임을 확인하면 된다.

2013년 11월 30일 토요일

Multiple pub/sub collection in meteor

iron Router를 잘 쓰고 있는데 쓰다 보니 이상한 점이 있다.
페이지별로 subscribe 하는 것까지는 좋지만
당연히 여러 개의 page에 대해 subscribe를 해야 하는데
waitOn 같은 곳에서 여러 개의 collection에 대해 subscribe를 하다 보니 N번 화면을 갱신하기도 하고 코드도 좀 번거롭다.

여러 개의 Collection에 대한 pub/sub을 한 번에 할 수 없을까 생각하고 있었는데
https://github.com/meteor/meteor/pull/716
최근 내용을 보니까 구현이 되어있다!

이것도 undocumented라고 해야 하나 싶다.

아무튼, 좋다.
Posts, Replies 두 개의 Collection이 있다고 할 때 autopublish package를 meteor(혹은 mrt) remove autopublish하여 삭제하고 수동 가입을 구현한다면

/model/model.coffee
@Posts = new Meteor.Collection "posts"
@Replies = new Meteor.Collection "replies"

/client/subscribe.coffee
Meteor.subscribe 'posts-replies'

/server/publish.coffee
Meteor.publish 'posts', ->
  [Posts.find(), Replies.find()]

이렇게 [ ] 배열 형태로 묶어만 주면 된다.
좋은 패치라고 생각한다. 적극적으로 활용하자.

2013년 11월 25일 월요일

iron-router에서 /#/(hashbang) 을 사용할때 주의점( ex. verify-email등)

iron-router 적응 중인데 역시 대세다.
최근 server-side도 반영되어 구 router package에서 안심하고 넘어와도 되겠다 싶었다.

E-mail 인증(http://spectrumdig.blogspot.kr/2013/10/meteor-e-mail.html)을 구현해 놓았는데 이게 실제로 해보니까 iron-router 에선 제대로 되지 않는다.

찾아보니 Server 쪽에서 해당 URL이 들어올때 패치하는 방법이 있다.
https://github.com/EventedMind/iron-router/issues/3#issuecomment-20917649

Server 아래 Route 관련 부분에

Accounts.urls.resetPassword = (token) ->
  Meteor.absoluteUrl "reset-password/#{token}"
Accounts.urls.verifyEmail = (token) ->
  Meteor.absoluteUrl "verify-email/#{token}"
Accounts.urls.enrollAccount = (token) ->
  Meteor.absoluteUrl "enroll-account/#{token}"

이런 식으로 넣어주면 된다.

2013년 11월 7일 목요일

meteor에서 특정 조건에서 head를 조작하기

server/header.js 식으로 server에서만 작동하는 코드를 만들자.

Fiber = Npm.require("fibers");
WebApp.connectHandlers.use(function(req, res, next) {
if (req.url==='/connect') {
res.writeHead(200, {'Content-Type': 'text/html'});
Fiber(function() {
res.write('<html>')
res.write('<head>');
res.write('<meta property="qc:admins" content="2311172222411526554572637573766534" />');
res.write('<head>');
res.write('<body></body>');
res.write('</html>')
res.end();
}).run();
} else {
next();
}
});

위 코드는 weiyun 인증의 경우인데 <meta property="qc:admins" ... 를 사이트에서 요구하는 경우이다.
이런 식으로 meta를 요구하는 경우가 종종 있는데 아마 구글 서비스 중에도 있을 듯. 사이트 도구? 아마 그럴 것이다.

저번에도 이야기 했지만 WebApp.connectHandlers 요거 좋은 놈이다.
서버쪽에서 특정 url로 들어왔을때 예외처리를 하고 싶다면 이걸 사용하면 된다.
예의 경우는 /connect 로 들어왔을 때만 예외처리를 하고 나머지는 그냥 next()로 넘겼다.

여기서 주목할 점은 meteor는 기본적으로 fiber 처리를 하지만 이 경우는 그냥 node.js를 사용하는 것이라 block이 되지 않도록 Fiber를 처리해준다.

페이스북의 경우
if (req.headers['user-agent'].indexOf('facebookexternalhit') !== -1) {
                res.writeHead(200, {'Content-Type': 'text/html'});
이런 식으로 'user-agent'를 조사해주면 되겠다.

2013년 11월 1일 금요일

SoX Library 컴파일하기(for OS X)

./osxbuild 실행

오류
$ ruby -e "$(curl -fsSL https://raw.github.com/mxcl/homebrew/go)"
$ brew install automake

재시도

$ ./osxbuild
configure.ac:21: installing './compile'
configure.ac:10: installing './config.guess'
configure.ac:10: installing './config.sub'
configure.ac:12: installing './install-sh'
configure.ac:12: installing './missing'
libgsm/Makefile.am:35: error: Libtool library used but 'LIBTOOL' is undefined
libgsm/Makefile.am:35:   The usual way to define 'LIBTOOL' is to add 'LT_INIT'
libgsm/Makefile.am:35:   to 'configure.ac' and run 'aclocal' and 'autoconf' again.
libgsm/Makefile.am:35:   If 'LT_INIT' is in 'configure.ac', make sure
libgsm/Makefile.am:35:   its definition is in aclocal's search path.
libgsm/Makefile.am: installing './depcomp'
lpc10/Makefile.am:8: error: Libtool library used but 'LIBTOOL' is undefined
lpc10/Makefile.am:8:   The usual way to define 'LIBTOOL' is to add 'LT_INIT'
lpc10/Makefile.am:8:   to 'configure.ac' and run 'aclocal' and 'autoconf' again.
lpc10/Makefile.am:8:   If 'LT_INIT' is in 'configure.ac', make sure
lpc10/Makefile.am:8:   its definition is in aclocal's search path.
autoreconf: automake failed with exit status: 1
configure: WARNING: unrecognized options: --disable-shared
checking build system type... x86_64-apple-darwin13.0.0
checking host system type... x86_64-apple-darwin13.0.0
checking target system type... x86_64-apple-darwin13.0.0
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
checking whether make sets $(MAKE)... yes
checking whether make supports nested variables... yes
checking for gcc... gcc
checking whether the C compiler works... no
configure: error: in `/Users/spectrum/Documents/c/sox':
configure: error: C compiler cannot create executables
See `config.log' for more details
make: *** No rule to make target `all'.  Stop.


음. 기분이 나쁘군. 레퍼런스를 보고 하자.


mp3 까지 지원된다면 감사감사.
sox-14.4.1, lame-3.99.5, libmad-0.15.1b.  3종을 먼저 받으라고 하는데 libmad는 안쓰면 안되나? lame만 쓰고 싶은데 라이센스 문제도 싫고.

lame 해보자 lame

./configure CFLAGS="-m32 -arch i386" LDFLAGS="-arch i386" --enable-shared --disable-static
sudo make
sudo make install

별 탈 없이 잘된다.
$ lame --help
LAME 32bits version 3.99.5 (http://lame.sf.net)

usage: lame [options] <infile> [outfile]

    <infile> and/or <outfile> can be "-", which means stdin/stdout.

RECOMMENDED:
    lame -V2 input.wav output.mp3

OPTIONS:
    -b bitrate      set the bitrate, default 128 kbps
    -h              higher quality, but a little slower.  Recommended.
    -f              fast mode (lower quality)
    -V n            quality setting for VBR.  default n=4
                    0=high quality,bigger files. 9=smaller files
    --preset type   type must be "medium", "standard", "extreme", "insane",
                    or a value for an average desired bitrate and depending
                    on the value specified, appropriate quality settings will
                    be used.
                    "--preset help" gives more info on these

    --help id3      ID3 tagging related options

    --longhelp      full list of options

    --license       print License information

다행이야 다행.
친절하게 testcase.wav랑 mp3도 있네
testcase.mp3는 지우고 

$ lame testcase.wav
LAME 3.99.5 32bits (http://lame.sf.net)
Using polyphase lowpass filter, transition band: 16538 Hz - 17071 Hz
Encoding testcase.wav to testcase.mp3
Encoding as 44.1 kHz j-stereo MPEG-1 Layer III (11x) 128 kbps qval=3
    Frame          |  CPU time/estim | REAL time/estim | play/CPU |    ETA 
    23/23    (100%)|    0:00/    0:00|    0:00/    0:00|   9.7248x|    0:00 
-----------------------------------------------------------------------------------------------
   kbps        LR    MS  %     long switch short %
  128.0       95.7   4.3        78.3  13.0   8.7
Writing LAME Tag...done
ReplayGain: -1.6dB

잘됨. 만족

libmad도 해본다.
./configure CFLAGS="-m32 -arch i386" LDFLAGS="-arch i386" --enable-shared --disable-static
sudo make
sudo make install

이건 실행파일은 아니니까 존재유무만 확인하자.
$ ls /usr/local/lib/libmad*
/usr/local/lib/libmad.0.2.1.dylib /usr/local/lib/libmad.dylib
/usr/local/lib/libmad.0.dylib     /usr/local/lib/libmad.la

좋아. 이상없음.

./configure CFLAGS="-m32 -arch i386" LDFLAGS="-arch i386" --with-mad --with-lame

돌리다가
checking for style of include used by make... GNU
checking dependency style of gcc... gcc3
checking whether ln -s works... yes
./configure: line 4327: syntax error near unexpected token `dlopen'
./configure: line 4327: `LT_INIT(dlopen win32-dll)'

오류. win32-dll? 안쓸거니까 막자. # 붙여서 주석처리.
......
그래도 안된다.

이럴땐 다른 걸 받아보자.
에서 다시 받아서 재시도.

BUILD OPTIONS
Debugging build............no
Distro name ...............not specified!
Dynamic loading support....no
Pkg-config location........$(libdir)/pkgconfig
Play and rec symlinks......yes
Symlinks enabled...........yes

OPTIONAL DEVICE DRIVERS
ao (Xiph)..................no
alsa (Linux)...............no
coreaudio (Mac OS X).......yes
sndio (OpenBSD)............no
oss........................no
pulseaudio.................no
sunaudio...................no
waveaudio (MS-Windows).....no

OPTIONAL FILE FORMATS
amrnb......................no
amrwb......................no
ffmpeg.....................no
flac.......................no
gsm........................yes (in-tree)
lpc10......................yes (in-tree)
mp2/mp3....................yes
 id3tag....................no
 lame......................yes
 lame id3tag...............yes
 dlopen lame...............no
 mad.......................yes
 dlopen mad................no
 twolame...................no
oggvorbis..................no
sndfile....................no
wavpack....................no

OTHER OPTIONS
ladspa effects.............no
magic support..............no
png support................no
GOMP support...............no

Configure finished.  Do 'make -s && make install' to compile and install SoX.

성공인 느낌.
sudo make -s
엄청난 양의 warning; 하지만 에러는 없네.

$ sudo make install
Making install in lpc10
make[2]: Nothing to be done for `install-exec-am'.
make[2]: Nothing to be done for `install-data-am'.
Making install in libgsm
make[2]: Nothing to be done for `install-exec-am'.
make[2]: Nothing to be done for `install-data-am'.
Making install in src
 .././install-sh -c -d '/usr/local/lib'
 /bin/sh ../libtool --silent  --silent --mode=install /usr/bin/install -c   libsox.la '/usr/local/lib'
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ranlib: file: /usr/local/lib/libsox.a(libsox_la-ladspa.o) has no symbols
 .././install-sh -c -d '/usr/local/bin'
  /bin/sh ../libtool --silent  --silent --mode=install /usr/bin/install -c sox '/usr/local/bin'
/Applications/Xcode.app/Contents/Developer/usr/bin/make  install-exec-hook
if test "yes" = "yes"; then \
cd /usr/local/bin; rm -f  play rec; ln -s sox play; ln -s sox rec; \
fi
if test "yes" = "yes"; then \
cd /usr/local/bin; rm -f  soxi; ln -s sox soxi; \
fi
 .././install-sh -c -d '/usr/local/include'
 /usr/bin/install -c -m 644 sox.h '/usr/local/include'
make[2]: Nothing to be done for `install-exec-am'.
 ./install-sh -c -d '/usr/local/share/man/man1'
 /usr/bin/install -c -m 644 sox.1 soxi.1 '/usr/local/share/man/man1'
 ./install-sh -c -d '/usr/local/share/man/man3'
 /usr/bin/install -c -m 644 libsox.3 '/usr/local/share/man/man3'
 ./install-sh -c -d '/usr/local/share/man/man7'
 /usr/bin/install -c -m 644 soxformat.7 '/usr/local/share/man/man7'
 ./install-sh -c -d '/usr/local/lib/pkgconfig'
 /usr/bin/install -c -m 644 sox.pc '/usr/local/lib/pkgconfig'
/Applications/Xcode.app/Contents/Developer/usr/bin/make  install-data-hook
cd /usr/local/share/man/man1 && rm -f play.1 && ln -s sox.1 play.1
cd /usr/local/share/man/man1 && rm -f rec.1 && ln -s sox.1 rec.1
cd /usr/local/share/man/man7 && rm -f soxeffect.7 && ln -s ../man1/sox.1 soxeffect.7

어? 생각보다 어렵지 않게 잘 됨.
원래 잘 되는거구나;
확인.

$ sox
sox:      SoX v14.4.1

sox FAIL sox: Not enough input filenames specified

Usage summary: [gopts] [[fopts] infile]... [fopts] outfile [effect [effopt]]...

SPECIAL FILENAMES (infile, outfile):
-                        Pipe/redirect input/output (stdin/stdout); may need -t
-d, --default-device     Use the default audio device (where available)
-n, --null               Use the `null' file handler; e.g. with synth effect
-p, --sox-pipe           Alias for `-t sox -'

SPECIAL FILENAMES (infile only):
"|program [options] ..." Pipe input from external program (where supported)
http://server/file       Use the given URL as input file (where supported)

GLOBAL OPTIONS (gopts) (can be specified at any point before the first effect):
--buffer BYTES           Set the size of all processing buffers (default 8192)
--clobber                Don't prompt to overwrite output file (default)
--combine concatenate    Concatenate all input files (default for sox, rec)
--combine sequence       Sequence all input files (default for play)
-D, --no-dither          Don't dither automatically
--effects-file FILENAME  File containing effects and options
-G, --guard              Use temporary files to guard against clipping
-h, --help               Display version number and usage information
--help-effect NAME       Show usage of effect NAME, or NAME=all for all
--help-format NAME       Show info on format NAME, or NAME=all for all
--i, --info              Behave as soxi(1)
--input-buffer BYTES     Override the input buffer size (default: as --buffer)
--no-clobber             Prompt to overwrite output file
-m, --combine mix        Mix multiple input files (instead of concatenating)
--combine mix-power      Mix to equal power (instead of concatenating)
-M, --combine merge      Merge multiple input files (instead of concatenating)
--norm                   Guard (see --guard) & normalise
--play-rate-arg ARG      Default `rate' argument for auto-resample with `play'
--plot gnuplot|octave    Generate script to plot response of filter effect
-q, --no-show-progress   Run in quiet mode; opposite of -S
--replay-gain track|album|off  Default: off (sox, rec), track (play)
-R                       Use default random numbers (same on each run of SoX)
-S, --show-progress      Display progress while processing audio data
--single-threaded        Disable parallel effects channels processing
--temp DIRECTORY         Specify the directory to use for temporary files
-T, --combine multiply   Multiply samples of corresponding channels from all
                         input files (instead of concatenating)
--version                Display version number of SoX and exit
-V[LEVEL]                Increment or set verbosity level (default 2); levels:
                           1: failure messages
                           2: warnings
                           3: details of processing
                           4-6: increasing levels of debug messages
FORMAT OPTIONS (fopts):
Input file format options need only be supplied for files that are headerless.
Output files will have the same format as the input file where possible and not
overriden by any of various means including providing output format options.

-v|--volume FACTOR       Input file volume adjustment factor (real number)
--ignore-length          Ignore input file length given in header; read to EOF
-t|--type FILETYPE       File type of audio
-e|--encoding ENCODING   Set encoding (ENCODING may be one of signed-integer,
                         unsigned-integer, floating-point, mu-law, a-law,
                         ima-adpcm, ms-adpcm, gsm-full-rate)
-b|--bits BITS           Encoded sample size in bits
-N|--reverse-nibbles     Encoded nibble-order
-X|--reverse-bits        Encoded bit-order
--endian little|big|swap Encoded byte-order; swap means opposite to default
-L/-B/-x                 Short options for the above
-c|--channels CHANNELS   Number of channels of audio data; e.g. 2 = stereo
-r|--rate RATE           Sample rate of audio
-C|--compression FACTOR  Compression factor for output format
--add-comment TEXT       Append output file comment
--comment TEXT           Specify comment text for the output file
--comment-file FILENAME  File containing comment text for the output file
--no-glob                Don't `glob' wildcard match the following filename

AUDIO FILE FORMATS: 8svx aif aifc aiff aiffc al amb au avr cdda cdr cvs cvsd cvu dat dvms f32 f4 f64 f8 fssd gsm gsrt hcom htk ima ircam la lpc lpc10 lu maud mp2 mp3 nist prc raw s1 s16 s2 s24 s3 s32 s4 s8 sb sf sl sln smp snd sndr sndt sou sox sph sw txw u1 u16 u2 u24 u3 u32 u4 u8 ub ul uw vms voc vox wav wavpcm wve xa
PLAYLIST FORMATS: m3u pls
AUDIO DEVICE DRIVERS: coreaudio

EFFECTS: allpass band bandpass bandreject bass bend biquad chorus channels compand contrast dcshift deemph delay dither divide+ downsample earwax echo echos equalizer fade fir firfit+ flanger gain highpass hilbert input# loudness lowpass mcompand mixer* noiseprof noisered norm oops output# overdrive pad phaser pitch rate remix repeat reverb reverse riaa silence sinc speed splice stat stats stretch swap synth tempo treble tremolo trim upsample vad vol
  * Deprecated effect    + Experimental effect    # LibSoX-only effect
EFFECT OPTIONS (effopts): effect dependent; see --help-effect

멋있게 잘 나오네.
그럼 이제 이걸 crossbridge로 올려봐야지.

2013년 10월 25일 금요일

Genymotion 에서 Unroot/Root 상태 전환하기 위한 쉬운 방법.

Genymotion은 지옥 성능의 안드로이드 에뮬에 비해 확실히 쾌적하고 아름다운 툴이다.
최근 롬을 보면 기본 Su 설치 상태로 나오는데
일부 Root을 검사하는 앱에서 사용 제한이 걸리곤 한다.

멀티터치 같은 걸 시뮬하기 위해서 DroidMote를 사용한다던가 하는 용도를 봤을땐 Root가 필요하고
여러가지 이유로 개발환경에선 Root/Unroot를 다 쓸 수 있으면 좋다.

Genymotion Configuration 아이콘을 찍고
Save 버튼 위 Always allow su access (bypass Superuser app)에 체크박스를 켜준다.

Save 하면 아마 재부팅을 한다고 할 것이다. 재부팅하자.
요즘은 SuperSu 가 간단하니 SuperSu를 스토어에서 받자.

몇몇 가지를 묻는데 특별한 롬을 쓰느냐 물을 때 Normal 이랑 충돌이 있으니 기존 Su앱을 지우겠느냐에서 등등 상식적인 걸 체크하고

SuperSu를 다시 실행. 이제부턴 설정창에서 Su를 간편하게 껐다 켰다 할 수 있다.

2013년 10월 24일 목요일

meteor 에서 E-mail 인증하기


MAIL_URL은 gmail을 쓸 경우 다음과 같이 설정하면 된다.

smtp://사용자계정%40gmail.com:계정암호@smtp.gmail.com:465/

혹은 google apps 계정은 경우

smtp://사용자계정%40구글Apps도메인:계정암호@smtp.gmail.com:465/

----
가입자 확인 메일 발송엔 몇가지 문제가 있는데
1. Text로 메일을 보낸다.
2. Formatted 형식을 사용할 수 없다.

이건 내가 생각해봐도 좀 아니다 싶다.
server 쪽에서 startup 할때 기존 Accounts.emailTemplate과 Accounts.sendVerificationEmail 등등이 제대로 작동하도록 바꿔치기 해주자.

:: email.coffee

Meteor.startup ->
  # set Mail configuration
  Email = Package.email.Email;

  _.extend Accounts.emailTemplates,
    from: "Gearlounge <no-reply@gearlounge.com>"
    verifyEmail:
      subject: (user)->
        "#{"#{user.profile.displayName}님, "if (user.profile and user.profile.displayName)?}가입을 환영합니다."
      html: (user, url)->
        Handlebars.templates['verifyEmail'](
          user: user.profile && user.profile.displayName
          verifyEmailUrl: url
          urlHome: Meteor.absoluteUrl()
        )
  # hack for handlebars-server
  _.extend Accounts.sendVerificationEmail = (userId, address) ->
    user = Meteor.users.findOne userId
    throw new Error "Can't find user" if not user?
    if not address?
      email = _.find user.emails || [], (e) -> !e.verified
      address = (email || {}).address

    throw new Error "No such email address for user." if not address or not _.contains(_.pluck(user.emails || [], 'address'), address)

    tokenRecord =
      token: Random.id()
      address: address
      when: new Date()
    Meteor.users.update
      _id: userId
    ,
      $push:
        "services.email.verificationTokens": tokenRecord
    verifyEmailUrl = Accounts.urls.verifyEmail tokenRecord.token
    Email.send
      to: address
      from: Accounts.emailTemplates.from
      subject: Accounts.emailTemplates.verifyEmail.subject user
      html: Accounts.emailTemplates.verifyEmail.html user, verifyEmailUrl
    return

위 코드는 기존 accounts-password.js의 Accounts.emailTemplates을 그대로 옮겨(물론 coffeescript로 바꾸긴 했지만) 놓은 수준이다.
하지만 노랑색으로 그어 놓은 부분이 변경한 부분인데 기존 Accounts.emailTemplates는 text밖에 사용할 수 없어서 html을 인자로 주었고 메일 양식도 하드코딩된 부분을 handlebars-server(https://github.com/EventedMind/meteor-handlebars-server) 패키지를 사용하여 양식화하였다.

      html: (user, url)->
        Handlebars.templates['verifyEmail'](
          user: user.profile && user.profile.displayName
          verifyEmailUrl: url
          urlHome: Meteor.absoluteUrl()
        )

위 코드에서 보듯 verifyEmail 이란 template 을 사용하는데 server쪽 디렉토리 아래 적당히 verifyEmail.handlebars 라는 파일을 생성하면 알아서 잘 찾는다.
은근 괜찮음 추천추천.

서버사이드 핸들바도 클라이언트처럼 쓰면 된다. user, verifyEmailUrl, urlHome 을 각각 인자로 사용하는데 이 세개를 handlebars에서도 똑같이 이용한다.

<div>
<img src="{{urlHome}}image/header.img">
</div>
<p>
  안녕하세요. {{user}}님. 가입을 환영합니다.
</p>
<p>
  사용자 확인을 위해 아래 링크를 클릭해주세요.
</p>
<p>
  <a href="{{verifyEmailUrl}}" target="_blank">{{verifyEmailUrl}}</a>
</p>

이렇게 구성하면 오케이.
아마 verifyEmailUrl 은 아마도 http://localhost:3000/#/verify-email/HmfLCxjRKoRreqKWW 이런 형태일텐데

이제 이 경로로 오면 실제로 처리해주는 걸 만들어주면 되겠다.
클라이언트로 /verify-email/<token> 의 URL요청이 들어오면 처리하면 될텐데
내 경우엔 meteor-router(https://github.com/tmeasday/meteor-router) 패키지를 사용하므로
아래와 같이 구현했다.

Meteor.Router.add
....
  '/verify-email/:token':(token)->
    result = Meteor.call 'confirmEmail', token
    'confirmMail'

token을 인자로 'confirmEmail' method를 호출하면 해당계정의 email의 verified가 true가 되면서 사용가능한 상태로 만들 수 있다. 그리고 confirmMail 템플릿으로 포워딩하는 것으로 마무리.
만일 싱글페이지 앱이라면 url 분석해서 처리하는 방법도 있겠다.

물론 인증이 필요한 부분에 필터링을 한다던가 하는 세세한 부분의 구현이 있지만 실제로 해보면서 확인해보자.

2013년 10월 4일 금요일

Could not locate package.js 오류가 발생하면서 mrt/meteor 가 실행 안될때

/packages 디렉토리를 죄다 .gitignore로 등록해서 온 프로젝트가 있어서
지우고 다시 했더니 Could not locate package.js .. 오류가 발생했다.

https://github.com/DiscoverMeteor/Microscope/issues/11

이 글을 참조.

mrt uninstall --system

하고 다시 실행했더니 잘된다. 다행이네 다행이야.

2013년 10월 3일 목요일

node.js에서 angular.js 같은 동적 웹페이지를 사용할 때 crawling 문제

이전글 (http://spectrumdig.blogspot.kr/2013/10/spiderable-package.html)에서 meteor가 어떻게 봇들에게 렌더링된 이후의 html을 전달하는지 보았다.

근데 만일 node.js에서 같은 구현을 해야한다면?
역시 request를 까발려서 _escaped_fragment_ 가 있는지 user-agent가 어떤 것인지 분별하면 된다.

방법이야 여러가지가 있을 텐데
preprocessor를 사용하여 가로채서 조건에 맞으면 처리하고 아닐 경우 next()로 포워딩하는 방법이 있고 (https://github.com/acidsound/pushpot/blob/master/app.js#L30)

Express/Connect Middle-ware를 사용하여

app.use(function(req, res, next){
  if (req.query._escaped_fragment_ ||
    [/^facebookexternalhit/i, /^linkedinbot/i, /^twitterbot/i].some(function(v) {
      return v.test(req.headers['user-agent']);
    })) {
      // render by phantomJS
      res.send(getHTMLfromPhantomJS(req.url));
  } else {
    next();
  }

이런 식으로 처리해주면 되겠다.발로 짜서 실제 작동 여부는 모르겠다.
getHTMLfromPhantomJS 는 물론 직접 구현하시라 :p
https://github.com/meteor/meteor/blob/devel/packages/spiderable/spiderable.js 이런식으로 구현하면 됨.

spiderable package를 살펴보았다.

동적페이지를 만들면 웹크롤러들이 찌르러 왔다가 빈손으로 돌아가곤 한다.
이래서야 안될일.

발번역부터 시작해보자.
// list of bot user agents that we want to serve statically, but do
// not obey the _escaped_fragment_ protocol. The page is served
// statically to any client whos user agent matches any of these
// regexps. Users may modify this array.
//
// An original goal with the spiderable package was to avoid doing
// user-agent based tests. But the reality is not enough bots support
// the _escaped_fragment_ protocol, so we need to hardcode a list
// here. I shed a silent tear.
우리가 정적으로 제공하고 싶지만 _escaped_fragment_ 프로토콜을 따르지 않는 봇 사용자 에이전트의 목록입니다.
페이지는 사용자 에이전트가 이러한 정규식 중 하나와 일치하는 모든 클라이언트에 정적으로 제공합니다. 사용자는 이 배열을 변경할 수 있습니다.
spiderable 패키지와 원래 목표는 사용자 에이전트 기반의 테스트를 실시하는 것을 피하기 위해서였다.
하지만 현실은 봇들은 _escaped_fragment_ 프로토콜을 지원하지 않기 때문에, 여기에서는 목록을 하드 코딩해야합니다. 나는 조용히 눈물을 흘렸다.
Spiderable.userAgentRegExps = [
    /^facebookexternalhit/i, /^linkedinbot/i, /^twitterbot/i];

요놈들! 말을 듣지 않는 나쁜 아이들이 니 놈들이렸다! 이 오라질 것들!
아래와 같이 javascript 코드를 생성하여 phantom.js에 밀어 넣는다. 파일로에서 넣지 않고 바로 생성하기 위해 /dev/stdin 을 사용하여 밀어넣는다. 아마 이것 때문에 윈도우에선 애로사항이 꽃필 것이다. 방법이야 있지만 패스.

    var phantomScript = "var url = " + JSON.stringify(url) + ";" +
          "var page = require('webpage').create();" +
          "page.open(url);" +
          "setInterval(function() {" +
          "  var ready = page.evaluate(function () {" +
          "    if (typeof Meteor !== 'undefined' " +
          "        && typeof(Meteor.status) !== 'undefined' " +
          "        && Meteor.status().connected) {" +
          "      Deps.flush();" +
          "      return DDP._allSubscriptionsReady();" +
          "    }" +
          "    return false;" +
          "  });" +
          "  if (ready) {" +
          "    var out = page.content;" +
          "    out = out.replace(/<script[^>]+>(.|\\n|\\r)*?<\\/script\\s*>/ig, '');" +
          "    out = out.replace('<meta name=\"fragment\" content=\"!\">', '');" +
          "    console.log(out);" +
          "    phantom.exit();" +
          "  }" +
          "}, 100);\n";

소스 코드의 주 내용은 별게 없다. url을 인자로 받은 페이지를 만들어 반환하되 페이지 렌더링이 끝나는 시간을 가정하여 0.1초마다 계속 Meteor 어플리케이션의 존재를 확인하면 Deps를 flush하고 모든 subscription 들을 완료하였는지를 ready로 넘겨준다. 마지막으로 ready가 참이면 page.content를 넘겨주면서 끝난다.

단, <script>덩어리들을 홀랑 날리고 <meta name="fragment" content="!"> 도 홀랑홀랑 날린 다음에 stdout으로 보내주는 것이다. 평범한 html 문서 조각이 되는 것이다.

자. 큰덩어리로 돌아가는 구조를 말로 한번 풀어보자면 이런 것이다.

WebApp.connectHandlers 를 통해 들어온 놈들 중
request url이 _escaped_fragment_= 를 포함하거나  user-agent 이름이 facebookexternalhit, linkedinbot, twitterbot 같은 걸로 들어올 때
phantomJS를 가동해서 page.content를 얻은 다음 페이지로딩이 완료된 다음에 <script>와 <meta name="fragment" content="!"> 을 제거해서 돌려준다...
google 같은 경우 <meta name="fragment" content="!">가 있는 경우 _escaped_fragment_=를 붙여서 요청하는데 이때 phantomJS로 렌더링한 페이지 본문을 넘겨준다.

...라는 아름다운 이야기가 되겠다.

이래도 뭔소린지 모르겠다면 진리의 evetedmind 동영상을 보자.
https://www.eventedmind.com/posts/meteor-the-spiderable-package

재밌는 건 이 링크도 역시 spiderable 적용이다.
curl https://www.eventedmind.com/posts/meteor-the-spiderable-package
한 결과랑

curl https://www.eventedmind.com/posts/meteor-the-spiderable-package?_escaped_fragment_=
한 결과를 비교해보면 한눈에 알 수 있다.

마찬가지로 다른 bot들을 적용해보려면 user-agent를 주면 된다.

curl -s -A "facebookexternalhit" https://www.eventedmind.com/posts/meteor-the-spiderable-package

훌륭하지 않은가?

2013년 9월 9일 월요일

Meteor 에서 #constant를 사용하여 collection이 바뀌어도 특정 블록을 유지하기.

meteor는 collection을 기반으로 움직인다.
즉 collection이 변화가 있으면 해당하는 부분의 렌더링을 부분적으로 다시 하는데
고맙게도 preserve-input 이라는 패키지를 기본적으로 사용하여
전체를 다 로딩하지 않게끔 하고 있다.

<head>
  <title>replyExample</title>
</head>

<body>
  {{> Posts}}
</body>

<template name="Posts">
  <ul>
    {{#each posts}}
    <li>
      {{message}}
    </li>
    {{/each}}
  </ul>
  <input type="text" class="newPost"/>
</template>

최소한으로 컬렉션을 표시하는 html은 이런 형태일 것이다.

@Posts = new Meteor.Collection "posts"
if Meteor.isClient
  Template.Posts.posts = ->
    Posts.find {}

.coffee의 내용은 이렇게 되겠지.

하지만 재귀적으로 글>댓글 형식으로 구현할 경우 다소 문제가 된다.

<head>
  <title>replyExample</title>
</head>

<body>
  {{> Posts}}
</body>

<template name="Posts">
  <ul>
    {{#each posts}}
    <li>
      {{message}}
      {{#if replies}}
      <ul>
        {{#each replies}}
        <li>
          {{reply}}
        </li>
        {{/each}}
      </ul>
      {{/if}}
      <input type="text"/>
    </li>
    {{/each}}
  </ul>
</template>

이럴 경우 각각의 post마다 input 이 붙는데 update+$push를 사용하여 replies를 추가하면
Posts.update('<POST의 ID>',{$push:{"replies":{reply:"5"}}});
posts 부분을 다시 렌더링 하면서 #each replies 안쪽의 input 이 다시 렌더링되고 만다.
즉, 내가 댓글을 달려고 하는데 다른 곳에서 댓글을 달아버리는 경우 DDP를 통해 update 를 메시지를 받아 매번 input을 다시 그려버리게 된다.

isolate 를 쓴다던가 spark나 deps를 이용하는 방법도 있겠지만
이 경우는 실제로 <input type="text"/> 구간을 건드리지 않도록만 해도 된다.
http://docs.meteor.com/#constant 를 사용하여 적용해보자.

<head>
  <title>replyExample</title>
</head>

<body>
  {{> Posts}}
</body>

<template name="Posts">
  <ul>
    {{#each posts}}
    <li>
      {{message}}
      {{#if replies}}
      <ul>
        {{#each replies}}
        <li>
          {{reply}}
        </li>
        {{/each}}
      </ul>
      {{/if}}
      {{#constant}}
      <input type="text"/>
      {{/constant}}
    </li>
    {{/each}}
  </ul>
</template>

아주 간단하다.

별 상관은 없지만 https://www.eventedmind.com/posts/meteor-spark-isolate-annotation 같은 링크도 봐가면서 어떤 식으로 Meteor가 html 문서를 갱신하는지 구조를 봐두는 것도 좋다.

2013년 9월 2일 월요일

Meteor의 Login유지 방식

역시 마루타는 atmosphere이다.

한번 로그인 한 사이트에 갈때마다 여러번 로그인 하는 건 세련되지 않다.

어떤식으로 Meteor에서 로그인 유지를 하는지 보자.

잡았다 요놈! Local Storage에 저장한다. 이게 Session 객체인지 Local Storage를 삭제하니까 실시간으로 로그아웃이 해제된다 + _+)!!!
Meteor.loginToken으로 저장하고 확인하는구나.

https://github.com/meteor/meteor/blob/devel/packages/accounts-base/localstorage_token.js

내용을 보니 localStroage에 대한 fallback(https://github.com/meteor/meteor/blob/devel/packages/localstorage/localstorage.js)을 따로 만들어 놓았다.
켁, 소스를 보니 IE일 경우 userData를 사용하네!
http://msdn.microsoft.com/en-us/library/ms533007(v=vs.85).aspx
관련 내용 링크.




2013년 8월 27일 화요일

node.js 를 위한 vim 환경 설정.

tmux를 쓰다보니 vim 환경 설정 셋을 문득 만들어 보고 싶었다.

1. 일단 vim 먼저 받고

sudo apt-get install vim

2. manage your runtimepath 관리용 pathgen 설치

mkdir -p ~/.vim/autoload ~/.vim/bundle; \
curl -Sso ~/.vim/autoload/pathogen.vim \
    https://raw.github.com/tpope/vim-pathogen/master/autoload/pathogen.vim

3. ~/.vimrc 없을테니 아래와 같이 생성. tab/indent 간격은 2에 탭 대신 space로 쓰기 위해 expandtab 추가

execute pathogen#infect()
syntax on
filetype plugin indent on
set tabstop=2
set shiftwidth=2
set softtabstop=2
set expandtab

4. 아무 js 나 vim 으로 열어서 color 적용여부 확인

5. sensible plugin 설치

git clone git://github.com/tpope/vim-sensible.git ~/.vim/bundle/vim-sensible

6. node bundle 설치

git clone https://github.com/moll/vim-node.git ~/.vim/bundle/node

7. coffeescript bundle 설치

git clone https://github.com/kchmck/vim-coffee-script.git ~/.vim/bundle/vim-coffee-script/

8. zen code 보다 더 좋은 Emmet 설치 - ctrl+y+, 로 활성화

git clone http://github.com/mattn/emmet-vim.git ~/.vim/bundle/emmet-vim

9. less 도 활성화

git clone https://github.com/groenewege/vim-less ~/.vim/bundle/vim-less

뭐 여기까지 하면 되려나?
기왕 만든거 한방팩 추가
https://gist.github.com/acidsound/6346222

터미널 열고
curl https://gist.github.com/acidsound/6346222/raw/81f10ede441502f4b797189b2784bc81c6e514bd/set_vim.sh | /bin/sh

요거 한번 붙여주면 알아서 설치한다.

2013년 8월 25일 일요일

Meteor 에서 외부 Application으로 DDP 인증 (2) : SRP 교환

DDP로 인증을 하려면 네트웍상에 평문으로 보내지 않기 위해
SRP(Secure Remote Password) 라는 프로토콜로 변환한다.

https://github.com/meteor/meteor/tree/devel/packages/srp

package.js 를 보면 biginteger 와 sha256 을 사용한다.

구현해야할 부분은 https://github.com/meteor/meteor/blob/devel/packages/accounts-password/password_client.js#L10
이 곳 참조.

password 를 가지고 다음과 같이.

    s = new srp.Client password
    request = s.startExchange()
    if typeof selector == 'string'
      selector =
        if ~selector.indexof("@")
        then username: selector
        else email: selector
    request.user = selector

beginPasswordExchange 를 시작하는 부분의 인자는 request 자체를 사용.
request 내용은 A를 키로 하는 SRP 암호문과 user를 키로하는 사용자명이 되겠다.

method 쏴준다. A에 srp로 암호화한 문자를 사용.


{ msg: 'method',
  method: 'beginPasswordExchange',
  params:
   [ { A: '439284960ce6bc7c163262b5629a467cd06bb982432615ec526a1ed204d2f76a1a254faaab6c5c9309e120d708e3e46fe58d8c5017aedb85efb9670cfcbf8a8b2dd996a6226f7be032bfedc23b1261093c0374e3e176fb374818f8a38b015517c8bfc3cc1326967760bdeaac09e154e115826022cdcea75f944d61a7e724721a',
       user: {
         username: "......."  /* 혹은  email: "...." */
       } } ],
  id: '1' }

형식으로 보내면 되겠다.
코드를 써보면

  if dataJSON.msg is 'ready'
    console.log '>> method beginPasswordExchange'
    s = new srp.Client password
    request = s.startExchange()
    if typeof selector is 'string'
      selector =
        if ~selector.indexOf("@")
        then email: selector
        else username: selector
    request.user = selector
    sendObject =
      msg: 'method',
      method: 'beginPasswordExchange',
      params: [request]
      id: (++nextid).toString()
    ws.send JSON.stringify sendObject

ready 이후에 이런식으로 써주면 되겠다. id는 1 넣으니 Malformed method invocation 오류가 나니 sequence 값을 계속 사용하고 있다.

돌려보니

>> method beginPasswordExchange
{ msg: 'updated', methods: [ '2' ] }
{ msg: 'result',
  id: '2',
  result:
   { identity: 'YzHuNjbsWJm5KvDce',
     salt: 'sre6wg5ENP2FFNc42',
     B: '6bd07e3539b211ad523e38509501becba435618e25d6bd3f55852311a4c8d5a49cc0b29ffa8ccc28d3122a12b6ee20fefa01fd313fbe92c4200156e7faa7c21e3a43b3949fc7feaba9843c858f3144d92994261e7a86f9154593ec795c7725bf73e8e4fa2febda2a4fcb8bb84aa248a2ad2bb1c31d13bdc1bf025e3deec813aa' } }

하지만 이게 전부는 아니다.
서버에 갔다와서 해쉬를 받아오는 부분이라 유저명만 체크한다고 봐도 된다. 암호는 결과 값을 기준으로 확인해야함.

login method를 쏴보자.

{
  "msg":"method",
  "method":"login",
  "params":[
    {
      "srp":{
        "M":"e3e18d009a23a2f560e8eb157b6eca9d686dc4f95e2f0381ba9a8fb8dd4fa9de"
      }
    }
  ],
  "id":"2"
}

분석해보니 이런 내용이 websocket 나왔었다.
일단 여기까지 별 어려움 없이 잘 왔다.

어짜피 한 password 방식일때인 경우만 분석해봤는데
다른 경우도 크게 다르지 않을 거라고 생각한다.

전체 소스는 gist(https://gist.github.com/acidsound/6330360)에 올려놓겠다.

nextid = 0
connected = false;
ws = require 'ws'
## srp library from https://github.com/meteor/meteor/tree/devel/packages/srp
srp = require './lib/srp.js'
isLogin = false

## id/pass
selector = '<ID>'
password = '<PASS>'
s = new srp.Client password

ws = new ws 'ws://localhost:3000/websocket'
ws.on 'open', ->
  console.log "open socket"
  ws.send JSON.stringify
    msg: 'connect'
    version: 'pre1'
    support: ['pre1']
ws.on 'message', (data, flags)->
  dataJSON = JSON.parse data
  console.log dataJSON
  if dataJSON.msg is 'connected'
    connected = true
    ws.send JSON.stringify
      msg: 'sub'
      id: (++nextid).toString()
      name: 'meteor.loginServiceConfiguration'
  if dataJSON.msg is 'ready'
    if not isLogin
      console.log '>> method beginPasswordExchange'
      request = s.startExchange()
      if typeof selector is 'string'
        selector =
          if ~selector.indexOf("@")
          then email: selector
          else username: selector
      request.user = selector
      sendObject =
        msg: 'method'
        method: 'beginPasswordExchange'
        params: [request]
        id: (++nextid).toString()
      ws.send JSON.stringify sendObject
  if dataJSON.msg is 'result'
    if not isLogin
      console.log ">> checked username"
      if !!dataJSON.error
        console.log ">> username fail"
      else
        response = s.respondToChallenge dataJSON.result
        sendObject =
          msg: 'method'
          method: 'login'
          params: [
            srp: response
          ]
          id: (++nextid).toString()
        console.log JSON.stringify sendObject
        ws.send JSON.stringify sendObject
        isLogin = true
    else
      ## after login
      if !!dataJSON.error
        console.log ">>>> incorrect password"
      else
        if s.verifyConfirmation(HAMK:dataJSON.result.HAMK)
          console.log ">>>> login success"
        else
          console.log ">>>> server is cheating!"




2013년 8월 13일 화요일

Meteor 에서 외부 Application으로 DDP 인증.

http://stackoverflow.com/questions/16729992/authenticating-with-meteor-via-ddp-and-srp

Basic Auth로 websocket frame을 떠봤더니

o
1
오전 1:47:09
a["{\"server_id\":\"X43fG44wD7iJNCdoq\"}"]
42
오전 1:47:09
["{\"msg\":\"connect\",\"version\":\"pre1\",\"support\":[\"pre1\"]}"]
69
오전 1:47:09
["{\"msg\":\"sub\",\"id\":\"8jW8AaNcWf2iRhXWC\",\"name\":\"meteor.loginServiceConfiguration\",\"params\":[]}"]
110
오전 1:47:09
a["{\"msg\":\"connected\",\"session\":\"ZddaS7aatWT4i8nAR\"}"]
62
오전 1:47:09
a["{\"msg\":\"added\",\"collection\":\"users\",\"id\":\"aHC7AHZXbnhs7Efs9\",\"fields\":{\"username\":\"mone\",\"profile\":{\"photoSmall\":\"/image/tempImage/member_photo_placeholder.png\"}}}"]
192
오전 1:47:09
a["{\"msg\":\"added\",\"collection\":\"users\",\"id\":\"HuL3adLrtPKMoFWMK\",\"fields\":{\"profile\":{\"photoSmall\":\"/image/tempImage/member_photo_placeholder.png\"},\"username\":\"admin\"}}"]
193
오전 1:47:09
a["{\"msg\":\"added\",\"collection\":\"users\",\"id\":\"MZqYAk6C9kYao3ATz\",\"fields\":{\"profile\":{\"photoSmall\":\"/image/tempImage/member_photo_placeholder.png\"},\"username\":\"spectrum\"}}"]
196
오전 1:47:09
a["{\"msg\":\"ready\",\"subs\":[\"8jW8AaNcWf2iRhXWC\"]}"]
----------------------------------------------------------------------

여기까지 로그인 전

----------------------------------------------------------------------
"{\"msg\":\"method\",\"method\":\"login\",\"params\":[{\"srp\":{\"M\":\" \"}}],\"id\":\"2\"}"]
158
오전 1:48:22
a["{\"msg\":\"changed\",\"collection\":\"users\",\"id\":\"MZqYAk6C9kYao3ATz\",\"fields\":{\"emails\":[{\"address\":\"spectrick@gmail.com\",\"verified\":false}]}}"]
163
오전 1:48:22
a["{\"msg\":\"ready\",\"subs\":[\"8jW8AaNcWf2iRhXWC\"]}"]
57
오전 1:48:22
a["{\"msg\":\"updated\",\"methods\":[\"2\"]}"]
46
오전 1:48:22
a["{\"msg\":\"result\",\"id\":\"2\",\"result\":{\"token\":\"oGyFPfZM3c95yXw7M\",\"id\":\"MZqYAk6C9kYao3ATz\",\"HAMK\":\"b372e317d359b294cd9c5a4ca39c8f5523c310288661eba731191e2b4614ef17\"}}"]
190
오전 1:48:22
h

뭐 대략 이렇게 나오는데
a는 응답일테고

순서대로 보면
1. Connect 요구
{ msg: 'connect',
  version: 'pre1',
  support: [ 'pre1' ] }
이건 간단한데
{msg: "connected", session: "4vXhahuK5GTNzcC4S"}
이런 식으로 응답이 오면 오케이

2. 인증설정요구
{ msg: 'sub',
  id: 'xaiCo33DPqaEJhkbc',   /* id는 UUID. 아무거나 넣어도 된다. */
  name: 'meteor.loginServiceConfiguration',
  params: [] }

잠시 삼천포
UUID 생성은

Meteor.uuid = function () {
  var HEX_DIGITS = "0123456789abcdef";
  var s = [];
  for (var i = 0; i < 36; i++) {
    s[i] = Random.choice(HEX_DIGITS);
  }
  s[14] = "4";
  s[19] = HEX_DIGITS.substr((parseInt(s[19],16) & 0x3) | 0x8, 1);
  s[8] = s[13] = s[18] = s[23] = "-";

  var uuid = s.join("");
  return uuid;
}

Random.choice = function (arrayOrString) {
  var index = Math.floor(Random.fraction() * arrayOrString.length);
  if (typeof arrayOrString === "string")
    return arrayOrString.substr(index, 1);
  else
    return arrayOrString[index];
}

이런 식이다.
아무튼 sub 즉 subscribe 메시지로 meteor.loginServiceConfiguration 이란 이름의 collection을 구독 요청하면

{ server_id: 'X43fG44wD7iJNCdoq' }
{ msg: 'connected', session: 'EzRf563xHiK7J2FDo' }
{ msg: 'added',
  collection: 'users',
  id: 'aHC7AHZXbnhs7Efs9',
  fields: 
   { username: 'mone',
     profile: { photoSmall: '/image/tempImage/member_photo_placeholder.png' } } }
{ msg: 'added',
  collection: 'users',
  id: 'HuL3adLrtPKMoFWMK',
  fields: 
   { profile: { photoSmall: '/image/tempImage/member_photo_placeholder.png' },
     username: 'admin' } }
{ msg: 'added',
  collection: 'users',
  id: 'MZqYAk6C9kYao3ATz',
  fields: 
   { profile: { photoSmall: '/image/tempImage/member_photo_placeholder.png' },
     username: 'spectrum' } }
{ msg: 'ready', subs: [ '1' ] }

유저명이 나오고, 'ready'를 받으면 끝. atmosphere.meteor.com 은 user 명을 보내지 않더라. 뭔가 설정이 있을 듯.


보내는게 무엇인지 살펴보니
{ msg: 'method',
  method: 'login',
  params: [ { srp: 
    "M":"HASHHASHHASHHASHHASHHASHHASHHASHHASHHASH"
  } ],
  id: '2' }
이렇다.
참고로 atmosphere는

{ msg: 'method',
  method: 'login',
  params: 
   [ 'atm',
     '0.1.0',
     { srp: 
      "M":"HASHHASHHASHHASHHASHHASHHASHHASHHASHHASH"
     } ],
  id: '2' }

뭔가 조금 더 있다.

요걸 보내면 된다는 말인데
M에 사용자계정과 암호를 잘 어떻게 해서 응답이 돌아오면 되겠다.

M에 해당하는 부분은 SRP라는 것으로 암호화되어있는데
이걸 사용하면 된다.
로직은 그냥 소스 읽자.

nextid = 0
connected = false;
ws = require 'ws'
ws = new ws 'ws://localhost:3000/websocket'
ws.on 'open', ->
  console.log "open socket"
  ws.send JSON.stringify
    msg: 'connect'
    version: 'pre1'
    support: ['pre1']
ws.on 'message', (data, flags)->
  dataJSON = JSON.parse data
  console.log dataJSON
  if dataJSON.msg is 'connected'
    connected = true
    ws.send JSON.stringify
      msg: 'sub'
      id: (++nextid).toString()
      name: 'meteor.loginServiceConfiguration'
  if dataJSON.msg is 'ready'
    ws.send JSON.stringify
      msg: 'method'
      method: 'login',
      params: [
        srp:
          M: "*이 부분에 SRP로 인코딩"
      ]
      id: '2'

해봤는데 잘 안된다.
사실 이 부분이 핵심인데 Basic auth / OAuth 도 다 다를것이다.
OAuth를 쓰면 어짜피 웹으로 왔다갔다 해야해서 모바일 등에서 쓰기 곤란할 듯.
그럼 로그인은 웹에서 하고 인증된 후에 재로그인이 가능한지 확인해보자.

{ msg: 'method',
  method: 'login',
  params: [ { resume: 'MvzqjyYD9bCs3rrcb' } ],
  id: '1' }

resume 값을 기억하고 있으면 되는가보다 리프레쉬 했는데 같은 값을 반환한다.

open socket
{ server_id: 'X43fG44wD7iJNCdoq' }
{ msg: 'connected', session: 'ggEMcek5WmGD9vJSG' }
{ msg: 'added',
  collection: 'users',
  id: 'aHC7AHZXbnhs7Efs9',
  fields: 
   { username: 'mone',
     profile: { photoSmall: '/image/tempImage/member_photo_placeholder.png' } } }
{ msg: 'added',
  collection: 'users',
  id: 'HuL3adLrtPKMoFWMK',
  fields: 
   { profile: { photoSmall: '/image/tempImage/member_photo_placeholder.png' },
     username: 'admin' } }
{ msg: 'added',
  collection: 'users',
  id: 'MZqYAk6C9kYao3ATz',
  fields: 
   { profile: { photoSmall: '/image/tempImage/member_photo_placeholder.png' },
     username: 'spectrum' } }
{ msg: 'ready', subs: [ '1' ] }
{ msg: 'changed',
  collection: 'users',
  id: 'MZqYAk6C9kYao3ATz',
  fields: { emails: [ [Object] ] } }
{ msg: 'ready', subs: [ '1' ] }
{ msg: 'updated', methods: [ '11' ] }
{ msg: 'result',
  id: '11',
  result: { token: 'MvzqjyYD9bCs3rrcb', id: 'MZqYAk6C9kYao3ATz' } }
{ msg: 'result',
  id: '11',
  result: { token: 'MvzqjyYD9bCs3rrcb', id: 'MZqYAk6C9kYao3ATz' } }
{ msg: 'updated', methods: [ '11' ] }


음 token 이 어짜피 있어서 의미없나?
암튼 전체 소스
nextid = 0
connected = false;
ws = require 'ws'
ws = new ws 'ws://localhost:3000/websocket'
ws.on 'open', ->
  console.log "open socket"
  ws.send JSON.stringify
    msg: 'connect'
    version: 'pre1'
    support: ['pre1']
ws.on 'message', (data, flags)->
  dataJSON = JSON.parse data
  console.log dataJSON
  if dataJSON.msg is 'connected'
    connected = true
    ws.send JSON.stringify
      msg: 'sub'
      id: (++nextid).toString()
      name: 'meteor.loginServiceConfiguration'
  if dataJSON.msg is 'ready'
    ws.send JSON.stringify
      msg: 'method'
      method: 'login',
      params: [
        resume: 'MvzqjyYD9bCs3rrcb'
      ]
      id: '1'


2013년 7월 26일 금요일

intelliJ에서 actionscript3 사용시 외부 파일 문제.

source code 안에서 [Embed]를 사용하는 방법도 있지만
리소스를 실시간으로 로딩하여 초기 기동시간을 앞당기고 싶어서
Loader를 사용하여 swf를 실시간 로딩하였으나
데스크탑에뮬/iOS/Android가 각각 다르게 작동.

    private var stageLoader:Loader;
..
    stageLoader = new Loader();
    stageLoader.load(new URLRequest("NoiseInfoSession01.swf"));

이런식으로 로드를 하려고 했더니

Error #2044: Unhandled IOErrorEvent:. text=Error #2035: URL Not Found.

오류 나신다.

Project structure 에서 source를 추가했다.

기본은 src 폴더만 잡혀있는데 오른쪽 tree에서 loader에서 부를 swf가 있는 경로인 flash를 찍고 상단에 그 상단에 Sources 라고 써있는 부분을 클릭하면 Source Folders에 추가된다.

하지만 이게 전부가 아니다.
이 상태로는 장치에서 돌렸을때 역시 누락이 발생해서 URL오류가 난다.
URL 경로가 잘못되었나 싶어서 봤더니 아니다. 아예 패키징할때 빠지는 현상이다.
Android 와 iOS 탭 각각에 똑같이 추가할 파일들을 Path to file or folder 목록에 넣어주자.
iPhone 5 대응용 Default-568h@2x.png 스플래시 파일도 동일한 요령으로 추가하면 된다.

보통 gitignore 할때 *.iml을 빼주곤 하는데
as3를 할땐 그래선 안된다. 위의 설정이 iml 파일안에 들어가므로 반드시 git 목록에 추가해놓자.

요약
1. 외부 파일은 Modules에서 Android/iOS 각각 추가
2. 데스크탑 에뮬을 위해 해당 폴더를 Sources에도 추가
3. iml도 형상관리

2013년 7월 3일 수요일

Renoise로 바닥부터 Sound Design (No Sample/No VST)

Renoise는 Ableton Live Standard 버전처럼 기본적으로 아무 악기가 없고 달랑 샘플러 하나가 전부인데 그마나 다행인건 멀티레이어라고나 할까.
샘플러만 있으면 사실 다 되지. 아날로그 웨이브 테이블을 최소단위로 넣어서 루프를 돌리면  되니까.
근데 그러면 무조건 폴리포니가 되어서 구조적으로 모노 신스는 구현이 불가능하다.
그것도 방법이 없는 건 아닌데 Bend 라든가 Glide 등등으로 하면 되니까.
그래도 모노 신스가 있었으면 좋겠는데 방법이 있더라.
http://forum.renoise.com/index.php?/topic/27225-renoise-native-monophonic-synthesiser/ 이 글을 보고 약간 충격을 받음.

이펙터만 있으면 역시 소리를 만들 수 있구나!
바로 시도에 들어감.

처음은 이런 상태. 나는 누군가. 여긴 어딘가.
키보드를 눌러도 아무 소리가 안난다.
ESC 눌러서 첫번째 트랙 맨위에 Z(C-4 00) 하나 눌러 놓고 일단 플레이.
일단 이렇게 해놓고 소리가 날 수 있게 한단 말이지.

그럼 먼저 제네레이터.
일단 1byte짜리라도 뭐가 있어야 시작을 할 수 있으니 빈 샘플을 만들자.
1byte 짜리 빈 샘플을 만든다.
당연히 소리가 안난다.

DC Offset을 만들어 00 인 상태를 바꿔보자.
퍽하고 클릭음이 생기면서 Master Scope 에 변화가 생겼다.
Meter도 생겼다.
그런데 이건 소리라고 할 수 없다.
RingMod(Ring Modulator)를 추가해보자.
오오 소리가 난다.
0인 경우엔 Ring Modulator를 적용해도 0으로 소용이 없지만 DC Offset으로 값을 변경한 후부터 Oscillator에 따라 파형이 생긴다.
기본적으로 440Hz의 음을 들을 수 있다.
무에서 유를 만들기는 했는데
Pitch도 Volume도 없다.

일단 Pitch부터 해보자.


Key Tracker로 Destination을 RingMod에 Frequency로 주면 피치가 변한다.
하지만 음계가 맞지 않는다.
당연하다. 12음계가 주파수대에 골고루 분포해 있는 건 아니니까.
후우. 이 부분이 노가다였나보다.
그래서 LFO로 한번 더 거쳐서 Custom 모드로 0~119(C-0~B-9:12x10)까지 매핑을 해보자.
A 4->440hz 기준으로 해서 한땀한땀 작업한다.
http://www.phy.mtu.edu/~suits/notefreqs.html
이야 근데 이걸 어떻게 입력했지;;;;
xml 열어서 입력했나 - -);;
뭐 그렇게 입력할 수 있긴하지.
이 단계는 뭔가 Lua Script Editor 같은 걸로도 될텐데 어쨌든 테이블이 있어야 되니까 노가다는 마찬가지.

마무리로 Velocity 하나 있으면 좋겠다.
Ring Mod 의 Amount 값을 Tracking 하면 된다.
Velocity Tracker를 꺼내자.
Range가 00~7F인 점에 유의. 왜냐면 MIDI Velocity가 0~127까지니까.
Square가 부우우웅 올라온다 ㅋㅋㅋㅋㅋㅋㅋ
난 역시 Square가 좋아.
누가 들어도 난 이 세상에 없는 기계소리야 라고 말하는 것 같은 뚜렷한 정체성이 느껴지거든. 게다가 싸구려 같고.

2013년 6월 26일 수요일

Renoise 공부 - Recording Vocal / Interpolation / Multiple instance / Console

Renoise 는 기본이 Sample 를 Track에 찍는 것.
기준은 C4

일단 찍어놓고 LOOP을 돌리면서 shift+option+R 상태로 Sample Recorder를 기동
play 시 시작점을 지나지 않으면 소리가 나지 않는데
중간부터 재생해도 듣고 싶으면 Instrument Settings 에서 Auto seek을 활성화 해준다.

Renoise AutoSeek 영상 참조.

연속된 값을 계속 채워 놓고 싶을때 처음과 끝만 써주고
cmd + I 로 밀어 넣는다.

복수개의 renoise 를 열고 싶을 때
- package 안쪽에 있는 renoise 를 직접 실행

Scripting Terminal & Editor 활성화
- Help / Show the Preferences Folder
- config.xml ShowScriptingDevelopmentTools 항목 true. 저장 전 renoise를 닫을 것.
- 재시작 후 tool 메뉴에서 확인

Multiple instance
- /Applications/Renoise_Reg_Intel64.app/Contents/MacOS/Renoise
- 혹은 open -n /Applications/Renoise_Reg_Intel64.app/


전자는 log 를 볼 수 있고 후자는 그냥 실행한 것과 동일

2013년 6월 17일 월요일

Meteor 와 다른 언어/플랫폼과의 통신 수단인 DDP

Meteor 0.6.x 대부터 DDP 라는게 생겼는데
이걸로 Meteor application 과 다른 application 사이에 통신을 할 수 있는 인터페이스가 생겼다.
Mobile 에서도 예외는 없는데 가령 websocket으로 직접 meteor 서버랑 붙어서 데이터를 교환하는게 가능.
이게 참 매우매우 강력하면서도 단순한 것이.
collection에 subscribe 하는 것과 method 를 호출하는 것만으로 다른 언어에서도 meteor application처럼 쾌적하게 자료 교환을 할 수가 있다 :)

DDP Spec.
https://github.com/meteor/meteor/blob/master/packages/livedata/DDP.md

언어별 클라이언트
Node.js : https://github.com/alansikora/node-ddpclient
Objective-C : https://github.com/alansikora/objective-c_ddp-client
Ruby : https://github.com/tmeasday/ruby-ddp-client
Java : https://github.com/kutrumbo/java-ddp-client
Python : https://github.com/meteor/meteor/tree/master/examples/unfinished/python-ddp-client (얘는 왜;;;)
.NET : https://github.com/sonyarouje/DDPClient.NET

2013년 5월 19일 일요일

Meteor에서 REST API를 사용하는 방법 #2

server쪽 API를 router package를 사용해 쓰다보면 header 같은 걸 다루거나 할땐 너무 단순하게 만들어서 곤란할 수 있다.
이전에도 다룬 적(http://spectrumdig.blogspot.kr/2012/08/meteor-rest.html)이 있지만 __meteor_bootstrap__ 객체를 통해 접근하면 node.js 식의 접근이 가능하다.

Meteor update와 함께 Npm 객체가 생겼으니 이를 이용해 connect 객체를 사용해보자
http://www.slideshare.net/cjoudrey/building-your-first-node-app-with-connect-express
위 슬라이드를 한번 보면 이해에 도움이 될 것이다.

WebApp.connectHandlers 가 connect.createServer()라는 걸 기억하면 된다.



connect = Npm.require 'connect'
server = WebApp.connectHandlers
server.use connect.router (app)->
app.get '/info', (req,res)->
res.end "info"
app.get '/user/:id', (req,res)->
res.end "user id: #{req.params.id}"

이와 같은 코드를 server 디렉토리에 안에 넣거나 Meteor.isServer일때 실행하도록 하자.
파일 업로드나 쿼리 문자열 처리(ex: ?a=1&b=c)하려면
각각 bodyParser(http://www.senchalabs.org/connect/bodyParser.html) 와 query(http://www.senchalabs.org/connect/query.html)를 추가하는 것이 좋다.
위의 해당 링크에 보면 소스까지 공개 해놓아서 이해하기 쉬우니 필요한게 있으면 추가해보자. (가령 logger라든지...)



connect = Npm.require 'connect'
server = WebApp.connectHandlers
server.use connect.bodyParser()
server.use connect.query()
server.use connect.router (app)->
app.get '/info', (req,res)->
res.end "info"
app.get '/user/:id', (req,res)->
res.end "user id: #{req.params.id}"
app.post '/user', (req, res)->
res.end "user name: #{req.body.user.name}"
app.get '/search', (req,res)->
res.end "search : #{req.query.q}"



$ curl -d '{"user":{"name":"tj"}}' -H "Content-Typeon" http://localhost:3000/user
user name: tj


$ curl http://localhost:3000/search?q=something
search : something

위 두개의 curl 명령은 각각 bodyParser와 query모듈이 없으면 오류를 발생한다. body와 query 객체가 request 객체 안에 없으니까.

Npm이 그랬던 것 처럼 __meteor_bootstrap__.app 도 세련되게 포장해야하지 않나 싶은데 그건 또 그때 일 :)


* Updatehttps://github.com/tmeasday/meteor-router/blob/master/lib/router_server.js 소스를 보니 기존의 __meteor_bootstrap__.app 이 WebApp.connectHandlers 로 바뀌었다.
만일 패키지를 만들었고 그것이 이전 버전과 하위 호환을 유지하고 싶다면 require와 connect.createServer에 대해

@require = if Npm? then Npm.require else __meteor_bootstrap__.require
@server = if WebApp?.connectHandlers? then WebApp.connectHandlers else __meteor_bootstrap__.app

과 같이 정의해서 쓰는 것도 좋겠다.

2013년 5월 11일 토요일

rvictl로 iOS Network packet을 보자.

iOS 5이후 부턴 USB로 장비를 연결하고 rvictl이란 명령으로 네트워크 패킷을 볼 수 있다고 한다.


$ rvictl

rvictl [-h][-l][-s <udid1> ... <udidN>][-x <udid1> ... <udidN>]

Remote Virtual Interface Tool starts and stops a remote packet capture instance
for any set of attached mobile devices. It can also provide feedback on any attached
devices that are currently relaying packets back to this host.

Options:
-l, -L List currently active devices
-s, -S Start a device or set of devices
-x, -X Stop a device or set of devices

내용은 간단.
UDID 입력하고 -s 로 시작 -l로 확인 -x로 정지.

한번 해보자


$ rvictl -s <UDID>

Starting device <UDID> [SUCCEEDED]


이런 식으로 나오면 성공. USB연결을 꼭 확인하자. 참고로 iPhone Simulator 에서도 UDID만 있으면 작동한다는 이야기가 있음; 어디다 쓸진 모르겠지만;;

확인해보자.

$ rvictl -l

Current Active Devices:

[1] <UDID>

아까 그 UDID 가 목록에 있으면 성공.

ifconfig 로 잘 들어갔나 확인해보자.


$ ifconfig -l
lo0 gif0 stf0 en0 p2p0 utun0 rvi0



rvi0이 들어갔으면 성공.


$ ifconfig rvi0
rvi0: flags=3005<UP,DEBUG,LINK0,LINK1> mtu 0

이런 식이다.

그다음은?
뭐 wireshark를 쓰던지 tcpdump를 쓰던지 마음대로

$ tcpdump -n -t -i rvi0 -q -A tcp
로 뽑아봤더니 잘 나오더라.


2013년 4월 18일 목요일

Meteor 0.6.x 변경.

그동안 server-side 에서 fs 같은 객체를 쓰기 위해

@require = __meteor_bootstrap__.require
이렇게 만들어 썼었는데 이번 업데이트로 Npm을 정식지원하면서 require도 같이 변경되었습니다.

@require = Npm.require
이렇게 쓰면 됩니다.

https://github.com/meteor/meteor/blob/devel/History.md 참조


2013년 4월 13일 토요일

입코딩 : 비텍스트 파일에 대한 형상관리(CVS Problem with non-text files)



어렸을때 오락실용 게임을 만드는 회사에서 사운드관련 일을 한적이 있는데 당시 야마하 음원 칩을 쓰는 기판엔 음악 시퀀스를 텍스트로 받아서 미디를 텍스트로 바꿔서 올리는 툴을 만들었다.

만일 그 시절에 git+github 같은 툴이 있었다면 음원소스는 바이너리, 그 음원소스가 위치하는 메타 정보는 텍스트로 분리해서 음악관련 협업작업을 할 수도 있었을텐데 하는 생각이 들곤한다.

이건 사실 바이너리 형태인 모든 파일포멧의 한계랄까 어쩔 수가 없다. 사실 스프레드시트 같은 것도 csv 같은 경우는 텍스트라서 오토머지가 되거든. 나머지는 안되고. 오히려 xlsx 같이 XML 기반인 경우 압축해제만 하면 괜찮지.

생각해보면 xlsx, docx, pptx같은 XML 기반의 office 파일의 경우 저장 > 자동 압축해제 > COMMIT & PUSH 의 저장 기작과 FETCH > MERGE > office 생성 > 열기의 과정을 지원해주면 됨

실제로 말이 되게 하려면 XML간 Diff를 좀 친숙하게 기존 형식으로 비교해서 보여주면 될 것 같지만 배보다 배꼽이 더 큰 작업이 될 수도 있겠다 o><;; 비텍스트인 객체간의 차이를 보여주려면 골치 아플테고;;

vba같은 스크립트로 만들어서 system명령을 호출하게 하는 방식으로 가능하게 할 수 있을 것 같긴 한데. 이 모든 난관을 다 극복하고 난 다음에도 여전히 형상관리 시스템에 대한 사용자 교육등등 극복해야할 과제는 남아있다.

간만에 입코딩을 했는데 돈 안되니까 일단 하지말자로 잠정 결론 짓는 걸로. 으하하. 가끔 뻘 개발이 하고 싶을때 이렇게 글로 가끔 풀어서 생각해보는 것도 나쁘지 않네.

입코딩으로 수천만원의 비용을 절약했어!

2013년 4월 2일 화요일

contentEditable 사용시 execCommand 에서 Selection 문제 해결법

contentEditable로 WYSWYG 에디터를 만들일이 있어서 하다보니
생각보다 까다로와서 기록해둔다.

먼저 javascript 구현체
http://jsbin.com/erukis/6/edit


<!DOCTYPE html>
<html>
<head>
<script src="http://code.jquery.com/jquery.min.js"></script>
<link href="http://twitter.github.com/bootstrap/assets/css/bootstrap.css" rel="stylesheet" type="text/css" />
<link href="http://twitter.github.com/bootstrap/assets/css/bootstrap-responsive.css" rel="stylesheet" type="text/css" />
<script src="http://twitter.github.com/bootstrap/assets/js/bootstrap.js"></script>
<meta name="description" content="mobile" />
<meta charset=utf-8 />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title></title>
</head>
<body>
  <div class="edit" contentEditable="true">
    여기를 찍어서 수정가능
  </div>
  <button href="#myModal" role="button" class="btn" data-toggle="modal">set Color</button>
  <div id="myModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
  <div class="modal-header">
    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
    <h4 id="myModalLabel">set Color</h4>
  </div>
  <div class="modal-body">
    #<span id="color" contentEditable="true">000000</span>
  </div>
  <div class="modal-footer">
    <button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
    <button class="btn btn-primary save" data-dismiss="modal" data-result="save"> Save</button>
  </div>
</div>
</body>
</html>

HTML은 이렇고


var range, selection;
var getRange = function() {
  if (document.body.createTextRange) {
    range = document.body.createTextRange();
  } else if (window.getSelection) {
    selection = window.getSelection();
    range = selection.getRangeAt(0);
  }
  return range;
};
var setRange = function(range) {
  if (document.body.createTextRange) {
    range.select();
  } else {
    selection.removeAllRanges();
    selection.addRange(range);
  }
};
$("#myModal").on("hide", function(e) {
  // 이쪽이 click 이벤트 이후에 발생?
//   selection.removeAllRanges();
}).on("shown", function(e) {
  // 여기서 선택 영역을 백업
  range = getRange();
  return true;
});

$(".save]").click(function() {
  setRange(range);
  console.log($("#color").text());
  document.execCommand("ForeColor", false, $("#color").text());
});

javascript 는 이런 식
stackoverflow랑 microsoft, mozilla 사이트를 뒤져가면서 했는데.
IE 8 이하 일때랑 그렇지 않을때랑 영역을 잡는 방식이 다르다.

contentEditable 인 div 영역을 선택 후 modal을 띄워 해당 텍스트의 색을 execCommand 로 변경하는 예제인데
영역을 선택하고 칼라값을 입력하려면 선택영역의 포커스를 잃어버리기 때문에 execCommand 실행 시점에 적용할 수 없게 된다.
그래서 세운 작전은

  1. 선택한 영역을 백업한다.
  2. save버튼을 누를 시 백업한 선택 영역을 재적용한다.
  3. execCommand 로 색상을 적용한다.

이렇게 구현했다.

시행착오가 있었지만 일단 chrome, FF, IE 9, Safari 등에서 작동을 확인했다.

http://jsbin.com/erukis/7/edit
소스를 coffeescript로 바꾸고 Range class를 만들었다.


class Range
  setRange: =>
    if document.body.createTextRange
      @range.select()
    else
      @selection.removeAllRanges();
      @selection.addRange @range
  getRange: =>
    if document.body.createTextRange
      @range = document.body.createTextRange()
    else
      @selection = window.getSelection()
      @range = @selection && @selection.getRangeAt 0
    return

range = new Range

$("#myModal").on "shown", ->
  range.getRange()
  true

$(".save").click ->
  range.setRange()
  document.execCommand "ForeColor", false, $("#color").text()
  true

역시 커피로 보면 깔끔해서 좋다.
분기를 한건 document.body에 createTextRange 함수가 있는지 여부에 따라 IE8 이하 버전의 처리를 하도록 했는데
document.body.createTextRange()를 하면 body 전체가 선택이 되어 문제가 있다.

document.selection.createRange() 로 변경하고 다소 수정을 해보니

http://jsbin.com/erukis/11/edit


class Range
  setRange: =>
    if document.selection
      @range.select()
    else
      @selection.removeAllRanges();
      @selection.addRange @range
  getRange: =>
    if document.selection
      @range = document.selection.createRange()
    else
      @selection = window.getSelection()
      @range = @selection && @selection.getRangeAt 0
    return

range = new Range

saveStatus = false

$("#myModal").on "shown", ->
  range.getRange()
  saveStatus = false
  true

$("#myModal").on "hidden", ->
  if saveStatus
    range.setRange()
    document.execCommand "ForeColor", false, $("#color").text()
  true

$(".save").click ->
  saveStatus = true
  true


이런 형태가 되었다.
오히려 IE 8 이전 버전의 방식이 더 좋아보인다.
  1. document.selection.createRange() 로 현재 range를 잡고 (물론 multiSelect가 아닌 가정)
  2. @range.select()로 1에서 받은 객체를 기준으로 select 함수를 통해 다시 선택
인셈이다.

달라진 점이 하나 있는데 save 버튼을 누르는 순간 IE는 포커스가 문제가 있는지 execCommand가 적용되지 않아
시점을 modal이 완전히 닫히고 난 다음에 하도록 flag를 주어서 처리하였다.
(hide도 안된다 transition 이 끝난 이후인 hidden으로 해야한다)

만일 이런 삽질이 애초에 싫다면 rangy(https://code.google.com/p/rangy/) 같은 cross-browser library를 사용한다면 좀 더 깔끔하게 할 수 있다.

html 에서 
<script src="http://rangy.googlecode.com/svn/trunk/dev/rangy-core.js"></script>
를 삽입하여 rangy 라이브러리를 가지고 오자.

class Range
  setRange: =>
    @selection.removeAllRanges()
    @selection.addRange @range
    return
  getRange: =>
    @selection = rangy.getSelection()
    @range = @selection.getRangeAt 0
    return

range = new Range

saveStatus = false

$("#myModal").on "shown", ->
  range.getRange()
  saveStatus = false
  true

$("#myModal").on "hidden", ->
  if saveStatus
    range.setRange()
    document.execCommand "ForeColor", false, $("#color").text()
  true

$(".save").click ->
  saveStatus = true
  true

if문을 없애서 좀 더 깔끔한 코드가 되었다.

2013년 3월 31일 일요일

meteorite 기동시 spawn ENOENT 오류가 날때




OS X를 Lion 에서 Moutain Lion 으로 업그레이드 후 mrt를 가동시켰더니

$ mrt

Stand back while Meteorite does its thing
smart.json changed.. installing from smart.json

Installing Meteor

  branch: https://github.com/meteor/meteor.git#master

Installing smart packages


events.js:72
        throw er; // Unhandled 'error' event
              ^
Error: spawn ENOENT
    at errnoException (child_process.js:948:11)
    at Process.ChildProcess._handle.onexit (child_process.js:739:34)

이런 오류가 날때가 있다.

spawn 을 하다가 오류가 났다는 건 외부 파일 실행을 하는 게 실패해서 인가 싶어서 보니
meteorite은 git을 사용하고 있었지. 그랬었지.

which git 해보니 없었다.

http://git-scm.com/ 여기 가서 다운받고 설치하자.
평화적으로 잘 해결이 되었다.

사실 해보고 나서 생각해보니 OS X에서 업글을 할때 git path를 잡아놓았던게 없어져서 그런 것인데
path 에 /usr/local/git/bin를 추가해주기만 해도 해결된다.

git을 다시 설치하면 /etc/paths.d/git 아래에 path를 잡아준다.

sudo sh -c 'which git > /etc/paths.d/git'
이런 식으로 잡아줘도 무방.