nodectl 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +2 -0
  4. data/CHANGELOG.md +24 -0
  5. data/Gemfile +4 -0
  6. data/README.md +3 -0
  7. data/Rakefile +1 -0
  8. data/TODO.md +7 -0
  9. data/assets/javascript/application.coffee +22 -0
  10. data/assets/javascript/bootstrap.js +2006 -0
  11. data/assets/javascript/controllers/application_controller.coffee +73 -0
  12. data/assets/javascript/controllers/service_controller.coffee +62 -0
  13. data/assets/javascript/event_handler.coffee +51 -0
  14. data/assets/javascript/models/action.coffee +28 -0
  15. data/assets/javascript/models/instance.coffee +7 -0
  16. data/assets/javascript/models/service.coffee +75 -0
  17. data/assets/javascript/models/version.coffee +3 -0
  18. data/assets/javascript/notifier.coffee +64 -0
  19. data/assets/javascript/router.coffee +3 -0
  20. data/assets/javascript/routes/application.coffee +6 -0
  21. data/assets/javascript/routes/log.coffee +4 -0
  22. data/assets/javascript/routes/service.coffee +3 -0
  23. data/assets/javascript/store.coffee +32 -0
  24. data/assets/javascript/templates/application.hbs +82 -0
  25. data/assets/javascript/templates/index.hbs +1 -0
  26. data/assets/javascript/templates/log.hbs +1 -0
  27. data/assets/javascript/templates/service.hbs +110 -0
  28. data/assets/javascript/templates/views/action.hbs +20 -0
  29. data/assets/javascript/templates/views/actions_list.hbs +26 -0
  30. data/assets/javascript/templates/views/environments_list.hbs +9 -0
  31. data/assets/javascript/templates/views/install_button.hbs +1 -0
  32. data/assets/javascript/templates/views/mass_run.hbs +30 -0
  33. data/assets/javascript/templates/views/notifier_control.hbs +7 -0
  34. data/assets/javascript/templates/views/param.hbs +6 -0
  35. data/assets/javascript/templates/views/terminal.hbs +8 -0
  36. data/assets/javascript/ui.coffee +24 -0
  37. data/assets/javascript/vendor/ansiparse.js +186 -0
  38. data/assets/javascript/vendor/audio.min.js +24 -0
  39. data/assets/javascript/vendor/audiojs.swf +0 -0
  40. data/assets/javascript/vendor/ember-data.js +10528 -0
  41. data/assets/javascript/vendor/ember.js +40583 -0
  42. data/assets/javascript/vendor/handlebars.js +2746 -0
  43. data/assets/javascript/vendor/jquery.js +8829 -0
  44. data/assets/javascript/vendor/ladda.js +157 -0
  45. data/assets/javascript/vendor/moment.min.js +6 -0
  46. data/assets/javascript/vendor/notify-osd.js +319 -0
  47. data/assets/javascript/vendor/player-graphics.gif +0 -0
  48. data/assets/javascript/vendor/spin.js +218 -0
  49. data/assets/javascript/views/action_view.coffee +18 -0
  50. data/assets/javascript/views/actions_list_view.coffee +2 -0
  51. data/assets/javascript/views/environments_list_view.coffee +2 -0
  52. data/assets/javascript/views/install_button_view.coffee +34 -0
  53. data/assets/javascript/views/mass_run_view.coffee +107 -0
  54. data/assets/javascript/views/notifier_control_view.coffee +29 -0
  55. data/assets/javascript/views/terminal_view.coffee +56 -0
  56. data/assets/stylesheets/application.css +137 -0
  57. data/assets/stylesheets/bootstrap-theme.css +347 -0
  58. data/assets/stylesheets/bootstrap.css +5785 -0
  59. data/assets/stylesheets/fonts/glyphicons-halflings-regular.eot +0 -0
  60. data/assets/stylesheets/fonts/glyphicons-halflings-regular.svg +229 -0
  61. data/assets/stylesheets/fonts/glyphicons-halflings-regular.ttf +0 -0
  62. data/assets/stylesheets/fonts/glyphicons-halflings-regular.woff +0 -0
  63. data/assets/stylesheets/ladda-themeless.css +330 -0
  64. data/assets/stylesheets/ladda.css +392 -0
  65. data/assets/stylesheets/notify-osd.css +49 -0
  66. data/assets/stylesheets/terminal.css +12 -0
  67. data/bin/nodectl +4 -0
  68. data/lib/nodectl.rb +137 -0
  69. data/lib/nodectl/action.rb +80 -0
  70. data/lib/nodectl/binding.rb +13 -0
  71. data/lib/nodectl/cli.rb +123 -0
  72. data/lib/nodectl/context.rb +34 -0
  73. data/lib/nodectl/database.rb +55 -0
  74. data/lib/nodectl/generators/init.rb +22 -0
  75. data/lib/nodectl/generators/templates/init/.gitignore +5 -0
  76. data/lib/nodectl/generators/templates/init/config/manifest.yml +1 -0
  77. data/lib/nodectl/generators/templates/init/config/server.yml +7 -0
  78. data/lib/nodectl/instance.rb +100 -0
  79. data/lib/nodectl/log.rb +13 -0
  80. data/lib/nodectl/manager.rb +71 -0
  81. data/lib/nodectl/multi_io.rb +16 -0
  82. data/lib/nodectl/options.rb +47 -0
  83. data/lib/nodectl/process.rb +145 -0
  84. data/lib/nodectl/promised_file.rb +37 -0
  85. data/lib/nodectl/recipe.rb +197 -0
  86. data/lib/nodectl/repository.rb +80 -0
  87. data/lib/nodectl/server.rb +103 -0
  88. data/lib/nodectl/service.rb +126 -0
  89. data/lib/nodectl/stream/buffer.rb +44 -0
  90. data/lib/nodectl/stream/events_session.rb +39 -0
  91. data/lib/nodectl/stream/file.rb +69 -0
  92. data/lib/nodectl/stream/file_session.rb +35 -0
  93. data/lib/nodectl/stream/file_with_memory.rb +17 -0
  94. data/lib/nodectl/stream/websocket.rb +90 -0
  95. data/lib/nodectl/version.rb +3 -0
  96. data/lib/nodectl/watchdog.rb +17 -0
  97. data/lib/nodectl/webapp/actions.rb +21 -0
  98. data/lib/nodectl/webapp/base.rb +29 -0
  99. data/lib/nodectl/webapp/instances.rb +46 -0
  100. data/lib/nodectl/webapp/public/sounds/alert.mp3 +0 -0
  101. data/lib/nodectl/webapp/public/sounds/error.mp3 +0 -0
  102. data/lib/nodectl/webapp/public/sounds/question.mp3 +0 -0
  103. data/lib/nodectl/webapp/services.rb +39 -0
  104. data/lib/nodectl/webapp/settings.rb +7 -0
  105. data/lib/nodectl/webapp/ui.rb +19 -0
  106. data/lib/nodectl/webapp/versions.rb +12 -0
  107. data/lib/nodectl/webapp/views/ui.haml +20 -0
  108. data/nodectl.gemspec +35 -0
  109. data/spec/spec_helper.rb +12 -0
  110. data/spec/stream/buffer_spec.rb +33 -0
  111. metadata +365 -0
@@ -0,0 +1,218 @@
1
+ (function(root, factory) {
2
+ if (typeof exports == "object") module.exports = factory(); else if (typeof define == "function" && define.amd) define(factory); else root.Spinner = factory();
3
+ })(this, function() {
4
+ "use strict";
5
+ var prefixes = [ "webkit", "Moz", "ms", "O" ], animations = {}, useCssAnimations;
6
+ function createEl(tag, prop) {
7
+ var el = document.createElement(tag || "div"), n;
8
+ for (n in prop) el[n] = prop[n];
9
+ return el;
10
+ }
11
+ function ins(parent) {
12
+ for (var i = 1, n = arguments.length; i < n; i++) parent.appendChild(arguments[i]);
13
+ return parent;
14
+ }
15
+ var sheet = function() {
16
+ var el = createEl("style", {
17
+ type: "text/css"
18
+ });
19
+ ins(document.getElementsByTagName("head")[0], el);
20
+ return el.sheet || el.styleSheet;
21
+ }();
22
+ function addAnimation(alpha, trail, i, lines) {
23
+ var name = [ "opacity", trail, ~~(alpha * 100), i, lines ].join("-"), start = .01 + i / lines * 100, z = Math.max(1 - (1 - alpha) / trail * (100 - start), alpha), prefix = useCssAnimations.substring(0, useCssAnimations.indexOf("Animation")).toLowerCase(), pre = prefix && "-" + prefix + "-" || "";
24
+ if (!animations[name]) {
25
+ sheet.insertRule("@" + pre + "keyframes " + name + "{" + "0%{opacity:" + z + "}" + start + "%{opacity:" + alpha + "}" + (start + .01) + "%{opacity:1}" + (start + trail) % 100 + "%{opacity:" + alpha + "}" + "100%{opacity:" + z + "}" + "}", sheet.cssRules.length);
26
+ animations[name] = 1;
27
+ }
28
+ return name;
29
+ }
30
+ function vendor(el, prop) {
31
+ var s = el.style, pp, i;
32
+ if (s[prop] !== undefined) return prop;
33
+ prop = prop.charAt(0).toUpperCase() + prop.slice(1);
34
+ for (i = 0; i < prefixes.length; i++) {
35
+ pp = prefixes[i] + prop;
36
+ if (s[pp] !== undefined) return pp;
37
+ }
38
+ }
39
+ function css(el, prop) {
40
+ for (var n in prop) el.style[vendor(el, n) || n] = prop[n];
41
+ return el;
42
+ }
43
+ function merge(obj) {
44
+ for (var i = 1; i < arguments.length; i++) {
45
+ var def = arguments[i];
46
+ for (var n in def) if (obj[n] === undefined) obj[n] = def[n];
47
+ }
48
+ return obj;
49
+ }
50
+ function pos(el) {
51
+ var o = {
52
+ x: el.offsetLeft,
53
+ y: el.offsetTop
54
+ };
55
+ while (el = el.offsetParent) o.x += el.offsetLeft, o.y += el.offsetTop;
56
+ return o;
57
+ }
58
+ var defaults = {
59
+ lines: 12,
60
+ length: 7,
61
+ width: 5,
62
+ radius: 10,
63
+ rotate: 0,
64
+ corners: 1,
65
+ color: "#000",
66
+ direction: 1,
67
+ speed: 1,
68
+ trail: 100,
69
+ opacity: 1 / 4,
70
+ fps: 20,
71
+ zIndex: 2e9,
72
+ className: "spinner",
73
+ top: "auto",
74
+ left: "auto",
75
+ position: "relative"
76
+ };
77
+ function Spinner(o) {
78
+ if (typeof this == "undefined") return new Spinner(o);
79
+ this.opts = merge(o || {}, Spinner.defaults, defaults);
80
+ }
81
+ Spinner.defaults = {};
82
+ merge(Spinner.prototype, {
83
+ spin: function(target) {
84
+ this.stop();
85
+ var self = this, o = self.opts, el = self.el = css(createEl(0, {
86
+ className: o.className
87
+ }), {
88
+ position: o.position,
89
+ width: 0,
90
+ zIndex: o.zIndex
91
+ }), mid = o.radius + o.length + o.width, ep, tp;
92
+ if (target) {
93
+ target.insertBefore(el, target.firstChild || null);
94
+ tp = pos(target);
95
+ ep = pos(el);
96
+ css(el, {
97
+ left: (o.left == "auto" ? tp.x - ep.x + (target.offsetWidth >> 1) : parseInt(o.left, 10) + mid) + "px",
98
+ top: (o.top == "auto" ? tp.y - ep.y + (target.offsetHeight >> 1) : parseInt(o.top, 10) + mid) + "px"
99
+ });
100
+ }
101
+ el.setAttribute("role", "progressbar");
102
+ self.lines(el, self.opts);
103
+ if (!useCssAnimations) {
104
+ var i = 0, start = (o.lines - 1) * (1 - o.direction) / 2, alpha, fps = o.fps, f = fps / o.speed, ostep = (1 - o.opacity) / (f * o.trail / 100), astep = f / o.lines;
105
+ (function anim() {
106
+ i++;
107
+ for (var j = 0; j < o.lines; j++) {
108
+ alpha = Math.max(1 - (i + (o.lines - j) * astep) % f * ostep, o.opacity);
109
+ self.opacity(el, j * o.direction + start, alpha, o);
110
+ }
111
+ self.timeout = self.el && setTimeout(anim, ~~(1e3 / fps));
112
+ })();
113
+ }
114
+ return self;
115
+ },
116
+ stop: function() {
117
+ var el = this.el;
118
+ if (el) {
119
+ clearTimeout(this.timeout);
120
+ if (el.parentNode) el.parentNode.removeChild(el);
121
+ this.el = undefined;
122
+ }
123
+ return this;
124
+ },
125
+ lines: function(el, o) {
126
+ var i = 0, start = (o.lines - 1) * (1 - o.direction) / 2, seg;
127
+ function fill(color, shadow) {
128
+ return css(createEl(), {
129
+ position: "absolute",
130
+ width: o.length + o.width + "px",
131
+ height: o.width + "px",
132
+ background: color,
133
+ boxShadow: shadow,
134
+ transformOrigin: "left",
135
+ transform: "rotate(" + ~~(360 / o.lines * i + o.rotate) + "deg) translate(" + o.radius + "px" + ",0)",
136
+ borderRadius: (o.corners * o.width >> 1) + "px"
137
+ });
138
+ }
139
+ for (;i < o.lines; i++) {
140
+ seg = css(createEl(), {
141
+ position: "absolute",
142
+ top: 1 + ~(o.width / 2) + "px",
143
+ transform: o.hwaccel ? "translate3d(0,0,0)" : "",
144
+ opacity: o.opacity,
145
+ animation: useCssAnimations && addAnimation(o.opacity, o.trail, start + i * o.direction, o.lines) + " " + 1 / o.speed + "s linear infinite"
146
+ });
147
+ if (o.shadow) ins(seg, css(fill("#000", "0 0 4px " + "#000"), {
148
+ top: 2 + "px"
149
+ }));
150
+ ins(el, ins(seg, fill(o.color, "0 0 1px rgba(0,0,0,.1)")));
151
+ }
152
+ return el;
153
+ },
154
+ opacity: function(el, i, val) {
155
+ if (i < el.childNodes.length) el.childNodes[i].style.opacity = val;
156
+ }
157
+ });
158
+ function initVML() {
159
+ function vml(tag, attr) {
160
+ return createEl("<" + tag + ' xmlns="urn:schemas-microsoft.com:vml" class="spin-vml">', attr);
161
+ }
162
+ sheet.addRule(".spin-vml", "behavior:url(#default#VML)");
163
+ Spinner.prototype.lines = function(el, o) {
164
+ var r = o.length + o.width, s = 2 * r;
165
+ function grp() {
166
+ return css(vml("group", {
167
+ coordsize: s + " " + s,
168
+ coordorigin: -r + " " + -r
169
+ }), {
170
+ width: s,
171
+ height: s
172
+ });
173
+ }
174
+ var margin = -(o.width + o.length) * 2 + "px", g = css(grp(), {
175
+ position: "absolute",
176
+ top: margin,
177
+ left: margin
178
+ }), i;
179
+ function seg(i, dx, filter) {
180
+ ins(g, ins(css(grp(), {
181
+ rotation: 360 / o.lines * i + "deg",
182
+ left: ~~dx
183
+ }), ins(css(vml("roundrect", {
184
+ arcsize: o.corners
185
+ }), {
186
+ width: r,
187
+ height: o.width,
188
+ left: o.radius,
189
+ top: -o.width >> 1,
190
+ filter: filter
191
+ }), vml("fill", {
192
+ color: o.color,
193
+ opacity: o.opacity
194
+ }), vml("stroke", {
195
+ opacity: 0
196
+ }))));
197
+ }
198
+ if (o.shadow) for (i = 1; i <= o.lines; i++) seg(i, -2, "progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");
199
+ for (i = 1; i <= o.lines; i++) seg(i);
200
+ return ins(el, g);
201
+ };
202
+ Spinner.prototype.opacity = function(el, i, val, o) {
203
+ var c = el.firstChild;
204
+ o = o.shadow && o.lines || 0;
205
+ if (c && i + o < c.childNodes.length) {
206
+ c = c.childNodes[i + o];
207
+ c = c && c.firstChild;
208
+ c = c && c.firstChild;
209
+ if (c) c.opacity = val;
210
+ }
211
+ };
212
+ }
213
+ var probe = css(createEl("group"), {
214
+ behavior: "url(#default#VML)"
215
+ });
216
+ if (!vendor(probe, "transform") && probe.adj) initVML(); else useCssAnimations = vendor(probe, "animation");
217
+ return Spinner;
218
+ });
@@ -0,0 +1,18 @@
1
+ UI.ActionView = Ember.View.extend
2
+ templateName: 'views/action'
3
+ classNames: ['dropdown']
4
+
5
+ didInsertElement: ->
6
+ unless @get('action')
7
+ throw new Error("'action' parameter required for ActionView")
8
+
9
+ @resize_handler = =>
10
+ height = $(window).height() - 170
11
+ @$('.terminal-output').height(height)
12
+
13
+ $(window).on('resize', @resize_handler)
14
+ @resize_handler()
15
+
16
+
17
+ willDestroyElement: ->
18
+ $(window).off('resize', @resize_handler)
@@ -0,0 +1,2 @@
1
+ UI.ActionsListView = Ember.View.extend
2
+ templateName: 'views/actions_list'
@@ -0,0 +1,2 @@
1
+ UI.EnvironmentsListView = Ember.View.extend
2
+ templateName: 'views/environments_list'
@@ -0,0 +1,34 @@
1
+ UI.InstallButtonView = Ember.View.extend
2
+ templateName: 'views/install_button'
3
+
4
+ actions:
5
+ installService: () ->
6
+ spinner = Ladda.create(@$('button')[0])
7
+ spinner.start()
8
+ if @get('service')
9
+ spinner.setProgress(1)
10
+ @get('service').run_slot 'install', {}, ->
11
+ spinner.stop()
12
+ else
13
+ spinner.setProgress(0)
14
+
15
+ @get('controller.store').find('service').then (list) ->
16
+ count = list.get('length')
17
+ ready = -1
18
+
19
+ install_next = ->
20
+ ready += 1
21
+ spinner.setProgress((1 / count) * ready)
22
+ if ready == count
23
+ spinner.stop()
24
+ return
25
+
26
+ service = list.objectAt(ready)
27
+ console.log "#{ready} from #{count}: ", service.get('id')
28
+
29
+ if service.get('install_supported')
30
+ service.run_slot 'install', {}, install_next
31
+ else
32
+ install_next()
33
+
34
+ install_next()
@@ -0,0 +1,107 @@
1
+ UI.MassRunView = Ember.View.extend
2
+
3
+ inst_options: {}
4
+
5
+ currentEnvironmentObserver: (->
6
+ console.log 'set to ', UI.get('currentEnvironment').toString()
7
+ @set('inst_options.env', UI.get('currentEnvironment').toString())
8
+ ).observes('UI.currentEnvironment')
9
+
10
+ templateName: 'views/mass_run'
11
+ services: []
12
+ servicesRunnable: []
13
+ servicesSelected: Ember.Set.create()
14
+
15
+ servicesRunnable: (->
16
+ @get('services').filterBy('start_supported', true)
17
+ ).property('services')
18
+
19
+ runCheckbox: Ember.Checkbox.extend
20
+ change: ->
21
+ list = @get('list')
22
+ service = @get('service')
23
+
24
+ if @get('checked')
25
+ list.add(service)
26
+ else
27
+ list.remove(service)
28
+
29
+ true
30
+
31
+ checkedSetHook: (->
32
+ list = @get('list')
33
+ service = @get('service')
34
+
35
+ if list.contains(service)
36
+ @set('checked', true)
37
+ else
38
+ @set('checked', false)
39
+ ).observes("list.length")
40
+
41
+
42
+ selectAllCheckbox: Ember.Checkbox.extend
43
+ attributeBindings: ['indeterminate']
44
+ indeterminate: false
45
+
46
+ click: ->
47
+ list = @get('list')
48
+ services = @get('services')
49
+
50
+ beforeChecked = @get('checked')
51
+ beforeIndetermine = @get('indeterminate')
52
+
53
+ if list.length == services.get('length')
54
+ @get('parent').send('unselectAll')
55
+ else if list.length > 0
56
+ @get('parent').send('unselectAll')
57
+ else
58
+ @get('parent').send('selectAll')
59
+
60
+ beforeChecked != @get('checked') || beforeIndetermine != @get('indeterminate')
61
+ true
62
+
63
+ checkedSetHook: (->
64
+ list = @get('list')
65
+ services = @get('services')
66
+
67
+ if list.length == services.get('length')
68
+ @set('checked', true)
69
+ @$().prop('indeterminate', false)
70
+ else if list.length > 0
71
+ @set('checked', true)
72
+ @$().prop('indeterminate', true)
73
+ else
74
+ @set('checked', false)
75
+ @$().prop('indeterminate', false)
76
+ ).observes("list.length")
77
+
78
+
79
+ didInsertElement: ->
80
+ unless @get('services')
81
+ throw "MassRunView cannot be instantiated without services"
82
+ @set('inst_options.env', UI.get('currentEnvironment').toString())
83
+
84
+ willDestroyElement: ->
85
+ @send('unselectAll')
86
+
87
+ actions:
88
+ runSelected: ->
89
+ @get('servicesSelected').forEach (service) =>
90
+ instance = service.get('store').createRecord 'instance', service: service, options: @get('inst_options')
91
+ instance.save().then =>
92
+ service.reload()
93
+ @send('unselectAll')
94
+ @set('controller.massRunOpened', false)
95
+
96
+ selectAll: ->
97
+ @get('servicesRunnable').forEach (service) =>
98
+ @get('servicesSelected').add(service)
99
+
100
+ unselectAll: ->
101
+ @get('servicesSelected').clear()
102
+
103
+ selectIdle: ->
104
+ @send('unselectAll')
105
+ @get('servicesRunnable').forEach (service) =>
106
+ if !service.get('isRunning')
107
+ @get('servicesSelected').add(service)
@@ -0,0 +1,29 @@
1
+ UI.NotifierControlView = Ember.View.extend
2
+ templateName: 'views/notifier_control'
3
+ tagName: ''
4
+
5
+ soundEnabled: false
6
+ popupEnabled: false
7
+
8
+ didInsertElement: ->
9
+ @notifier = window.notifier
10
+
11
+ @set('soundEnabled', @notifier.isSoundEnabled())
12
+ @set('popupEnabled', @notifier.isPopupEnabled())
13
+
14
+ actions:
15
+ toggleSound: ->
16
+ if @notifier.isSoundEnabled()
17
+ @notifier.soundDisable()
18
+ @set('soundEnabled', false)
19
+ else
20
+ @notifier.soundEnable()
21
+ @set('soundEnabled', true)
22
+
23
+ togglePopup: ->
24
+ if @notifier.isPopupEnabled()
25
+ @notifier.popupDisable()
26
+ @set('popupEnabled', false)
27
+ else
28
+ @notifier.popupEnable()
29
+ @set('popupEnabled', true)
@@ -0,0 +1,56 @@
1
+ UI.TerminalView = Ember.View.extend
2
+ templateName: 'views/terminal'
3
+ status: "connecting..."
4
+
5
+ path_hook: (->
6
+ @websocketReopen()
7
+ ).observes("path")
8
+
9
+ didInsertElement: ->
10
+ unless @get('path')
11
+ throw new Error("'path' parameter required for TerminalView")
12
+
13
+ @terminal = @$(".terminal-output")
14
+ @websocketReopen()
15
+
16
+ willDestroyElement: ->
17
+ @ws.close()
18
+
19
+ writeMessage: (message) ->
20
+ for part in ansiparse(message)
21
+ text = @escape(part.text).split("\n").join("<br>")
22
+ @write(text)
23
+
24
+ writeLn: (line) ->
25
+ @write(line + "<br>")
26
+
27
+ write: (text) ->
28
+ @terminal.append(text)
29
+ @terminal[0].scrollTop = @terminal[0].scrollHeight
30
+
31
+ websocketReopen: ->
32
+ @ws.close() if @ws
33
+ @terminal.empty()
34
+
35
+ if @get('path')
36
+ @ws = new WebSocket("#{window.settings.ws}#{@get('path')}");
37
+
38
+ @ws.onopen = =>
39
+ @set('status', 'connection etablished')
40
+
41
+ @ws.onerror = (event) =>
42
+ @set('status', "error: [#{event.code}] #{event.reason}")
43
+
44
+ @ws.onclose = (event) =>
45
+ @set('status', "connection closed: [#{event.code}] #{event.reason}")
46
+
47
+ @ws.onmessage = (message) =>
48
+ @writeMessage(message.data)
49
+
50
+ escape: (string) ->
51
+ $('<div/>').text(string).html()
52
+
53
+ actions:
54
+ cleanup: ->
55
+ @terminal.empty()
56
+