1. firefly
  2. eldis

Commits

firefly  committed c3977e2

MAde command execution parallel; pipelined commands now all start at the same time, and have to wait
for data through a "socket" system (basically event emitters supporting the 'data' and 'end' events).
Added TODO file.

  • Participants
  • Parent commits ffbab6c
  • Branches default

Comments (0)

Files changed (4)

File meta/todo

View file
+
+#### Eldis TODO #####################################################
+*  Commands are functions that may be annotated, e.g. with @synchronous or @background.
+*  Synchronous functions are like (regular) async ones, except that @done is called
+   automagically when they exit.
+*  Background functions: todo
+
+Command execution:
+*  Run command function
+*  Command function listens to @input%data (data received on input)
+*  Command function listens to @input%end  (input stream closed)
+*  Command produces output to @output (stdout)
+*  Command produces error output to @error (stderr)
+*  Command finishes by calling @done
+
+Protocol (example session):
+S  { type:'handshake', version:'0.0.1' }    # Handshake
+C  { type:'handshake', version:'0.0.1' }
+
+... # Someone executes 'echo 1 4 | square-sum'
+S  { type:'execution', eid:42, command:'square-sum', arguments:[] }
+S  { type:'input',   eid:42,  value:1  }
+C  { type:'output',  eid:42,  value:1  }
+S  { type:'input',   eid:42,  value:4  }
+C  { type:'output',  eid:42,  value:16 }
+S  { type:'input-end', eid:42 }
+C  { type:'output',  eid:42,  value:17 }
+C  { type:'output-end', eid:42 }
+
+# If a command doesn't need to produce output anymore
+S  { type:'status',  eid:42, value:'stop' }

File modules/core.coffee

View file
 @commands =
-	"echo": ->
-		@output.apply @, arguments
+	"echo": (exec) ->
+		exec.end.apply exec, Array::slice.call arguments, 1
 
-	"cat": ->
-		@output.apply @, @input
+	"cat":  (exec) ->
+		exec.stdin.on 'data', (data) ->
+			exec.output data
 
-	"tac": ->
-		@output.apply @, @input.reverse()
+	"rev":  @manual (exec) ->
+		outputBuffer = []
 
-	"sel": (pattern) ->
-		results = require('../deps/JSONSelect').match pattern, @input[0]
-		@output result for result in results
+		exec.stdin.on 'data', (data) ->
+			console.log "Unshifting", data
+			outputBuffer.unshift data
 
-	"grep": (pattern) ->
+		exec.stdin.on 'end', ->
+			console.log "Outputting", outputBuffer
+			exec.end.apply exec, outputBuffer
+
+	"sel":  (exec, selector) ->
+		exec.stdin.on 'data', (data) ->
+			results = require('../deps/JSONSelect').match selector, data
+			exec.output result for result in results
+
+	"grep": (exec, pattern) ->
 		regex = new RegExp pattern
 
-		for line in @input
-			if line.match regex
-				@output line
+		exec.stdin.on 'data', (data) ->
+			if data.match regex
+				exec.output data
 	
-	"sed": (pattern) ->
+	"sed":  (exec, pattern) ->
 		match = pattern.match ///
 			s (.)
 			( [^\1]+ ) \1
 			throw new Error "Invalid regex pattern."
 
 		[_, _, matcher, replacement, flags] = match
-		@output @input[0].replace RegExp(matcher, flags), replacement
+		regex = RegExp matcher, flags
 
-	"eval": (expr) ->
-		@output eval expr
+		exec.stdin.on 'data', (data) ->
+			exec.output data.replace regex, replacement
+
+	"eval": (exec, expr) ->
+		exec.output eval expr
 
 	"addCommand": (name, argNames...) ->
 		functionBody = require('coffee-script').compile @input.join "\n"
 		bot.reloadModule module for module in bot.modules
 		@output "Done!"
 
-	"g": @async ->
+	"g": @manual ->
 		query = encodeURIComponent Array::join.call arguments, " "
 
 		require('http').get {

File src/bot.coffee

View file
 
 		catch err
 			server.connection.write "PRIVMSG", target, "Error: #{err.message}"
+			console.log err.stack
 
 bot =
 	modules: []
 			code = require('coffee-script').compile res.toString()
 
 			ctx =
-				require : require
-				console : console
-				bot     : bot
+				require  : require
+				console  : console
+				bot      : bot
+				commands : {}
 
 				#### Command 'decorators' ###############################
-				async : (fn) ->
-					fn.asynchronous = true
+				manual : (fn) ->
+					fn.manualEnd = true
 					fn
 			
 			try

File src/shell.coffee

View file
 # FireFly shell thing
+{EventEmitter} = require 'events'
+
+DEBUG_EXECUTIONS = false
 
 exports.Shell = Shell = (opts = {}) ->
 	@defaults =
-		input  : opts.input or []
+		input  : opts.input
 		output : opts.output or =>
 			Array::push.apply @outputBuffer, arguments
 
 		++idx
 	
 	line = str[last..]
-	#line = line[0..-2] if mode != 'none'
-	#	line = line[1..-2]
 	
 	push 'literal', line if line.length > 0
 
 	res
 
 Shell::createExecution = ->
-	{
-		input  : @defaults.input[0..] # We want a copy
-		output : @defaults.output
-	}
+	res = {}
+
+	res.stdin  = new EventEmitter
+	res.stdout = new EventEmitter
+
+	#res.input  = res.stdin
+	res.output = ->
+		@stdout.emit 'data', data for data in arguments
+	
+	res
 
 Shell::process = (tokens) ->
 	res = []
 
 				else if value == "|"
 					newExecution = @createExecution()
-					curr.output = ->
-						Array::push.apply newExecution.input, arguments
+					curr.stdout = new EventEmitter
+
+					if DEBUG_EXECUTIONS
+						#### DEBUG ##################################
+						do (curr) ->
+							curr.stdout.on 'data', (data) ->
+								console.log "#{curr.command} -> #{newExecution.command} [data]:", data
+								newExecution.stdin.emit 'data', data
+					
+							curr.stdout.on 'end', ->
+								console.log "#{curr.command} -> #{newExecution.command} [end]"
+								newExecution.stdin.emit 'end'
+						#### /DEBUG #################################
+
+					else
+						curr.stdout = newExecution.stdin
 
 					res.push curr
 					curr = newExecution
 	res
 
 Shell::exec = (str, callback) ->
-	[first, rest...] = @process lex str
+	executions   = @process lex str
+	first        = executions[0]
+	last         = executions[-1..][0]
+	outputBuffer = []
 
-	@outputBuffer = []
+	console.log "==== executions ===="
+	console.log executions
 
-	execute = (ex, rest) =>
+	# No commands, nothing to do...
+	if executions.length == 0
+		callback null, []
+		return
+	
+	# Listen on stdout on last command; we want to catch output and
+	# send to channel.
+	last.stdout.on 'data', (str) ->
+		console.log "#{last.command} -> <output> [data]:", str if DEBUG_EXECUTIONS
+		outputBuffer.push str
+	
+	last.stdout.once 'end', ->
+		console.log "#{last.command} -> <output> [end]" if DEBUG_EXECUTIONS
+		callback null, outputBuffer
+
+	# Starts execution of one command.
+	execute = (ex) =>
+		console.log "Begin", ex.command if DEBUG_EXECUTIONS
 		func = @commands[ex.command]
 
 		if not func
 			throw new Error "Command not found: #{ex.command}"
-		
-		ex.stdout ?= console.log
-		ex.done = =>
-			if rest.length > 0
-				execute rest[0], rest[1..]
-			else
-				callback null, @outputBuffer # FIXME: Collect output here and pass to callback!
 
-		func.apply ex, ex.args
-		ex.done() if not func.asynchronous
+		ex.end = ->
+			console.log "End", ex.command if DEBUG_EXECUTIONS
+			@output.apply this, arguments
+			@stdout.emit 'end'
 
-	callback null, @outputBuffer if not first
+			# Clean up functions.
+			@end    = ->
+			@output = ->
 
-	execute first, rest
+		if not func.manualEnd
+			ex.stdin.on 'end', ->
+				ex.end()
 
-#console.log lex 'echo "hello, \'foo\' world!"'
-#(new Shell).exec 'echo Hello world'
+		# The process.nextTick is necessary for "asynchronous" behaviour
+		#process.nextTick ->
+		->
+			func.apply ex, [ex].concat ex.args
 
+	funcs = (execute ex for ex in executions)
+	do func for func in funcs.reverse()
+	first.stdin.emit 'end'
+