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 +7 -0
- data/lib/node_task/index.js +136 -0
- data/lib/node_task/node_task.rb +278 -0
- data/lib/node_task/package.json +15 -0
- data/lib/node_task.rb +1 -0
- metadata +67 -0
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: []
|