node_task 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []