node_task 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fa1be3ec14616f90bb984d79d5a640c62cf90019
4
+ data.tar.gz: 70950fdd92e3f1232467045782572dec5d1a76b1
5
+ SHA512:
6
+ metadata.gz: ee53d8c32c41ecfe33d18fb326654aeb419b8ed6a0d6e697625141d6baaea80b145063a57fa9f2873ffcb4f6bb462bbd552b79fb444413eb3dbb69f745dd7574
7
+ data.tar.gz: ea631053a8531cc1d100da7dd34c35eec85a7739aa2dcebf162fe9a6c6a43339020860abc2664bfce929b23bfba8900f6c46d320b67960f8b05f2c0b102636ed
@@ -0,0 +1,136 @@
1
+ var fs = require('fs')
2
+ var path = require('path')
3
+ var net = require('net')
4
+ var winston = require('winston')
5
+ var ndjson = require('ndjson')
6
+
7
+ var workingDir = path.resolve(process.env.NODE_TASK_CWD || __dirname)
8
+ var errorLogPath = path.join(workingDir, 'ruby_node_task-error.log')
9
+
10
+ // write pidfile
11
+ fs.writeFileSync(path.join(workingDir, 'ruby_node_task.pid'), process.pid+'')
12
+
13
+ var logger = makeLogger(workingDir, process.env.NODE_TASK_DEBUG)
14
+
15
+ var sockPath = process.env.NODE_TASK_SOCK_PATH || makeSockPath(
16
+ workingDir,
17
+ process.env.NODE_TASK_DAEMON_ID || 'ruby_node_task'
18
+ )
19
+
20
+ var server = net.createServer(onClientConnect)
21
+ .on('error', function (err) { logger.error('server error: '+err.toString()) })
22
+ .listen(sockPath, function() { logger.debug('listening on '+sockPath) })
23
+
24
+ process.on('exit', onExit)
25
+ process.on('uncaughtException', onUncaughtException)
26
+
27
+ // the important part
28
+ function onClientConnect(socket) {
29
+ logger.debug('client connected')
30
+
31
+ var busy = false
32
+ socket
33
+ .on('error', function(err) { logger.error('socket error: '+err.toString()) })
34
+ .on('end', function() { logger.debug('client finished') })
35
+ .on('close', function() { logger.debug('client disconnect') })
36
+ .pipe(ndjson.parse())
37
+ .on('error', sendError)
38
+ .on('data', receiveMsg)
39
+
40
+ function sendMsg(msg) { socket.end(serialiseMsg(msg)) }
41
+ function sendError(err) { socket.end(serialiseMsg(errorMsg(err))) }
42
+ function receiveMsg(msg) {
43
+ if (busy) {
44
+ return sendError(new Error('only one task can be run per connection'))
45
+ }
46
+
47
+ if (msg.status) {
48
+ return server.getConnections(function(err, count) {
49
+ sendMsg({clients: count-1}) // minus this connection
50
+ })
51
+ }
52
+
53
+ if (!msg.task) {
54
+ return sendError(new Error('msg.task not defined'))
55
+ }
56
+
57
+ var runTask = loadTaskModule(path.join(workingDir, msg.task))
58
+
59
+ var opts = msg.opts || {}
60
+ busy = true
61
+ runTask(opts, function(err, result) {
62
+ busy = false
63
+ if (err) {
64
+ logger.error('task error: '+err)
65
+ sendError(err)
66
+ } else {
67
+ logger.debug('task complete: '+msg.task)
68
+ sendMsg({result: result})
69
+ }
70
+ })
71
+ }
72
+
73
+ function loadTaskModule(taskModule) {
74
+ try {
75
+ return require(taskModule)
76
+ } catch (err) {
77
+ return sendError(new Error('Encountered "'+err+'" while attempting to load task "'+taskModule+'"'))
78
+ }
79
+ }
80
+ }
81
+
82
+ function onUncaughtException(err) {
83
+ logger.error('uncaught exception: '+err.toString(), function () {
84
+ try {
85
+ fs.writeFileSync(errorLogPath, err.stack)
86
+ } catch (err) {
87
+ return logger.error('logging error: '+err.toString(), function () {
88
+ process.exit()
89
+ })
90
+ }
91
+ process.exit()
92
+ })
93
+ }
94
+
95
+ function onExit() {
96
+ try {
97
+ fs.unlinkSync(sockPath)
98
+ logger.debug('removed '+sockPath)
99
+ } catch (err) {
100
+ // already removed
101
+ }
102
+ }
103
+
104
+ function makeLogger(workingDir, debug) {
105
+ return new winston.Logger({
106
+ transports: [
107
+ debug ? new winston.transports.File({
108
+ filename: path.join(workingDir, 'ruby_node_task-debug.log'),
109
+ level: 'debug',
110
+ }) : new winston.transports.Console(),
111
+ ],
112
+ })
113
+ }
114
+
115
+ // make a path for a cross platform compatible socket path
116
+ function makeSockPath(dir, name) {
117
+ if (process.platform === 'win32') {
118
+ return '\\\\.\\pipe\\'+name+'\\' + path.resolve(dir)
119
+ } else {
120
+ return path.join(dir, name+'.sock')
121
+ }
122
+ }
123
+
124
+ function serialiseMsg(msg) {
125
+ return JSON.stringify(msg)+'\n'
126
+ }
127
+
128
+ function errorMsg(err) {
129
+ return {
130
+ error: {
131
+ message: err.message,
132
+ code: err.code,
133
+ stack: err.stack,
134
+ }
135
+ }
136
+ }
@@ -0,0 +1,278 @@
1
+ require 'socket'
2
+ require 'json'
3
+ require 'daemon_controller'
4
+ require 'timeout'
5
+ require 'logger'
6
+
7
+ class NodeTask
8
+ RESPONSE_TIMEOUT = 20
9
+ START_MAX_RETRIES = 1
10
+
11
+ class Error < StandardError
12
+ def initialize(js_error)
13
+ @js_error = js_error
14
+ super(js_error[:message])
15
+ end
16
+
17
+ def to_s
18
+ @js_error[:stack] || @js_error[:message]
19
+ end
20
+ end
21
+
22
+ class << self
23
+ attr_writer :node_command
24
+ attr_writer :logger
25
+ attr_writer :working_dir
26
+
27
+ def windows?
28
+ (/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil
29
+ end
30
+
31
+ def logger
32
+ return @logger unless @logger.nil?
33
+ @logger = Logger.new(STDERR)
34
+ @logger.level = ENV["NODE_TASK_DEBUG"] ? Logger::DEBUG : Logger::INFO
35
+ @logger
36
+ end
37
+
38
+ def working_dir
39
+ @working_dir || Dir.pwd
40
+ end
41
+
42
+ def error_log_file
43
+ File.join(working_dir, "#{daemon_identifier}-error.log")
44
+ end
45
+
46
+ def pid_file
47
+ File.join(working_dir, "#{daemon_identifier}.pid")
48
+ end
49
+
50
+ def gem_dir
51
+ @gem_dir ||= File.dirname(File.expand_path(__FILE__))
52
+ end
53
+
54
+ def daemon_identifier
55
+ 'ruby_node_task'
56
+ end
57
+
58
+ def socket_path
59
+ @socket_path ||= _make_sock_path(working_dir, daemon_identifier)
60
+ end
61
+
62
+ def node_command
63
+ @node_command || ENV["NODE_COMMAND"] || ENV["NODE_TASK_DEBUG"] ? 'node --debug' : 'node'
64
+ end
65
+
66
+ def npm_command
67
+ @npm_command || ENV["NPM_COMMAND"] || 'npm'
68
+ end
69
+
70
+ def daemon_start_script
71
+ File.join(gem_dir, 'index.js').to_s
72
+ end
73
+
74
+ def npm_install
75
+ system("cd #{gem_dir}; #{npm_command} install")
76
+ end
77
+
78
+ # get configured daemon controller for daemon, and start it
79
+ def server
80
+ npm_install unless Dir.exists? File.join(gem_dir, 'node_modules')
81
+ @controller ||= _make_daemon_controller
82
+
83
+ begin
84
+ @controller.start
85
+ logger.debug "spawned server #{@controller.pid}"
86
+ rescue DaemonController::AlreadyStarted => e
87
+ logger.debug "server already running #{@controller.pid}"
88
+ end
89
+
90
+ @controller
91
+ end
92
+
93
+ # really try to successfully connect, starting the daemon if required
94
+ def ensure_connection
95
+ attempt = 0
96
+ begin
97
+ server # make sure daemon is running
98
+
99
+ socket = server.connect do
100
+ begin
101
+ _make_connection
102
+ rescue Errno::ENOENT => e
103
+ # daemon_controller doesn't understand ENOENT
104
+ raise Errno::ECONNREFUSED, e.message
105
+ end
106
+ end
107
+ rescue DaemonController::StartTimeout, DaemonController::StartError => e
108
+ logger.error e.message
109
+ if attempt < START_MAX_RETRIES
110
+ attempt += 1
111
+ logger.error "retrying attempt #{attempt}"
112
+ retry
113
+ else
114
+ raise e
115
+ end
116
+ end
117
+
118
+ socket
119
+ end
120
+
121
+ def check_error
122
+ if File.exist? error_log_file
123
+ # TODO: raise error
124
+ logger.error File.open(error_log_file).read
125
+ File.unlink error_log_file
126
+ true
127
+ end
128
+ end
129
+
130
+ # get a json response from socket
131
+ def parse_response(socket)
132
+ # only take one message - the result
133
+ # response terminated by newline
134
+ response_text = nil
135
+ loop do
136
+ response_text = socket.gets("\n")
137
+ break if response_text
138
+ break if check_error
139
+ end
140
+ if response_text
141
+ JSON.parse(response_text, symbolize_names: true)
142
+ else
143
+ logger.error 'no response for message'
144
+ nil
145
+ end
146
+ end
147
+
148
+ # make a single request, get a response and close the connection
149
+ def request(socket, message)
150
+ socket.write(message.to_json+"\n")
151
+
152
+ result = nil
153
+ begin
154
+ Timeout::timeout(RESPONSE_TIMEOUT) do
155
+ result = parse_response(socket)
156
+ end
157
+ rescue Timeout::Error, Exception => e
158
+ logger.error e.message
159
+ ensure
160
+ # disconnect after receiving response
161
+ socket.close
162
+ end
163
+
164
+ result
165
+ end
166
+
167
+ # number of connections active to the daemon
168
+ def clients_active
169
+ socket = _make_connection # might fail
170
+ message = {status: true} # special message type
171
+ result = request(socket, message)
172
+ return 0 if result.nil?
173
+ result[:clients]
174
+ end
175
+
176
+ # stop the daemon if no one else is using it
177
+ def release
178
+ begin
179
+ # this doesn't really work as intended right now
180
+ # as no connections are maintained when tasks aren't running
181
+ return if clients_active > 0
182
+ rescue Errno::ENOENT => e
183
+ # socket file probably doesn't exist
184
+ # maybe we should just return here?
185
+ end
186
+
187
+ pid = nil
188
+ begin
189
+ pid = @controller.pid
190
+ rescue Errno::ENOENT => e
191
+ # presumably no pid file exists and the daemon is not running
192
+ logger.debug "daemon already stopped"
193
+ return
194
+ end
195
+
196
+ logger.debug "stopping daemon #{pid}"
197
+ @controller.stop
198
+
199
+ begin
200
+ File.unlink socket_path
201
+ rescue Errno::ENOENT => e
202
+ # socket file's already gone
203
+ end
204
+ end
205
+
206
+ private
207
+
208
+ def _make_connection
209
+ UNIXSocket.new socket_path
210
+ end
211
+
212
+ def _make_sock_path(dir, name)
213
+ if windows?
214
+ "\\\\.\\pipe\\#{name}\\#{File.expand_path(dir)}"
215
+ else
216
+ File.join(dir, "#{name}.sock")
217
+ end
218
+ end
219
+
220
+ # TODO:
221
+ # - some server errors not reported
222
+ def _make_daemon_controller
223
+ logger.debug "socket_path #{socket_path}"
224
+
225
+ controller = DaemonController.new(
226
+ identifier: daemon_identifier,
227
+ start_command: "#{node_command} #{daemon_start_script}",
228
+ # ping_command: [:unix, socket_path],
229
+ ping_command: Proc.new{
230
+ begin
231
+ _make_connection
232
+ rescue Errno::ENOENT => e
233
+ # daemon_controller doesn't understand ENOENT
234
+ raise Errno::ECONNREFUSED, e.message
235
+ end
236
+ },
237
+ pid_file: pid_file,
238
+ log_file: error_log_file,
239
+ env: {
240
+ "NODE_TASK_SOCK_PATH" => socket_path,
241
+ "NODE_TASK_CWD" => working_dir,
242
+ "NODE_TASK_DAEMON_ID" => daemon_identifier,
243
+ "NODE_ENV" => ENV["RACK_ENV"],
244
+ },
245
+ start_timeout: 5,
246
+ daemonize_for_me: true,
247
+ )
248
+
249
+ at_exit { release }
250
+
251
+ controller
252
+ end
253
+ end
254
+
255
+ attr_accessor :task
256
+
257
+ def initialize(_task)
258
+ @task = _task
259
+ end
260
+
261
+ def run(opts = nil)
262
+ socket = self.class.ensure_connection
263
+
264
+ message = {
265
+ task: task,
266
+ opts: opts,
267
+ }
268
+
269
+ response = self.class.request(socket, message)
270
+ if response
271
+ if response[:error]
272
+ raise NodeTask::Error, response[:error]
273
+ else
274
+ response[:result]
275
+ end
276
+ end
277
+ end
278
+ end
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "ruby_node_task",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "author": "James Friend <james@jsdf.co> (http://jsdf.co/)",
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "ndjson": "^1.3.0",
13
+ "winston": "^1.0.0"
14
+ }
15
+ }
data/lib/node_task.rb ADDED
@@ -0,0 +1 @@
1
+ require_relative './node_task/node_task'
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: node_task
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - James Friend
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-06-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: daemon_controller
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.2'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.2.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.2'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.2.0
33
+ description: Much longer explanation of the node_task!
34
+ email: james@jsdf.co
35
+ executables: []
36
+ extensions: []
37
+ extra_rdoc_files: []
38
+ files:
39
+ - lib/node_task.rb
40
+ - lib/node_task/index.js
41
+ - lib/node_task/node_task.rb
42
+ - lib/node_task/package.json
43
+ homepage: https://rubygems.org/gems/node_task
44
+ licenses:
45
+ - MIT
46
+ metadata: {}
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubyforge_project:
63
+ rubygems_version: 2.4.3
64
+ signing_key:
65
+ specification_version: 4
66
+ summary: This is an node_task!
67
+ test_files: []