Przeglądaj źródła

Add hxd.net.RemoteConsole for local communication (#1267)

Yuxiao Mao 7 miesięcy temu
rodzic
commit
8055094e24
2 zmienionych plików z 296 dodań i 9 usunięć
  1. 281 0
      hxd/net/RemoteConsole.hx
  2. 15 9
      hxd/net/Socket.hx

+ 281 - 0
hxd/net/RemoteConsole.hx

@@ -0,0 +1,281 @@
+package hxd.net;
+
+/**
+	A simple socket-based local communication channel,
+	aim at communicate between 2 programs (e.g. Hide and a HL game).
+
+	Usage:
+	```haxe
+	var rcmd = new hxd.net.RemoteConsole();
+	// rcmd.log = (msg) -> logToUI(msg);
+	// rcmd.logError = (msg) -> logErrorToUI(msg);
+	rcmd.registerCommands(handler);
+	rcmd.connect(); // or rcmd.startServer()
+	rc.sendCommand("log", "Hello!", function(r) {});
+	```
+ */
+@:keep
+@:rtti
+class RemoteConsole {
+	public static var DEFAULT_HOST : String = "127.0.0.1";
+	public static var DEFAULT_PORT : Int = 40001;
+
+	var UID : Int = 0;
+	public var host : String;
+	public var port : Int;
+	var sock : Socket;
+	var cSocks : Array<Socket>;
+	var waitReply : Map<Int, Dynamic->Void>;
+
+	public function new( ?port : Int, ?host : String ) {
+		this.host = host ?? DEFAULT_HOST;
+		this.port = port ?? DEFAULT_PORT;
+		registerCommands(this);
+	}
+
+	public function startServer() {
+		close();
+		sock = new Socket();
+		sock.onError = function(msg) {
+			logError("Socket Error: " + msg);
+			close();
+		}
+		cSocks = [];
+		sock.bind(host, port, function(s) {
+			cSocks.push(s);
+			s.onError = function(msg) {
+				logError("Client error: " + msg);
+				if( s != null )
+					s.close();
+				if( s != null && cSocks != null ) {
+					cSocks.remove(s);
+				}
+			}
+			s.onData = () -> handleOnData(s);
+			log("Client connected");
+		}, 1);
+		log('Server started at $host:$port');
+	}
+
+	public function connect( ?onConnected : Bool -> Void ) {
+		close();
+		sock = new Socket();
+		sock.onError = function(msg) {
+			logError("Socket Error: " + msg);
+			close();
+			if( onConnected != null )
+				onConnected(false);
+		}
+		sock.onData = () -> handleOnData(sock);
+		sock.connect(host, port, function() {
+			log("Connected to server");
+			if( onConnected != null )
+				onConnected(true);
+		});
+		log('Connecting to $host:$port');
+	}
+
+	public function close() {
+		if( sock != null ) {
+			sock.close();
+			sock = null;
+		}
+		if( cSocks != null ) {
+			for( s in cSocks )
+				s.close();
+			cSocks = null;
+		}
+		UID = 0;
+		waitReply = [];
+	}
+
+	public function isConnected() {
+		return sock != null;
+	}
+
+	public dynamic function log( msg : String ) {
+		trace(msg);
+	}
+
+	public dynamic function logError( msg : String ) {
+		trace('[Error] $msg');
+	}
+
+	public function sendCommand( cmd : String, ?args : Dynamic, ?onResult : Dynamic -> Void ) {
+		var id = ++UID;
+		waitReply.set(id, onResult);
+		sendData(cmd, args, id);
+	}
+
+	function sendData( cmd : String, args : Dynamic, id : Int ) {
+		var obj = { cmd : cmd, args : args, id : id};
+		var bytes = haxe.io.Bytes.ofString(haxe.Json.stringify(obj) + "\n");
+		if( cSocks != null ) {
+			for( cs in cSocks ) {
+				cs.out.writeBytes(bytes, 0, bytes.length);
+			}
+		} else {
+			sock.out.writeBytes(bytes, 0, bytes.length);
+		}
+	}
+
+	function handleOnData( s : Socket ) {
+		var str = s.input.readLine().toString();
+		var obj = try { haxe.Json.parse(str); } catch (e) { logError("Parse error: " + e); null; };
+		if( obj == null || obj.id == null ) {
+			return;
+		}
+		var id : Int = obj.id;
+		if( id <= 0 ) {
+			var onResult = waitReply.get(-id);
+			waitReply.remove(-id);
+			if( onResult != null ) {
+				onResult(obj.args);
+			}
+		} else {
+			onCommand(obj.cmd, obj.args, (result) -> sendData(null, result, -id));
+		}
+	}
+
+	function onCommand( cmd : String, args : Dynamic, onDone : Dynamic -> Void ) : Void {
+		if( cmd == null )
+			return;
+		var command = commands.get(cmd);
+		if( command == null ) {
+			logError("Unsupported command " + cmd);
+			return;
+		}
+		command(args, onDone);
+	}
+
+	// ----- Commands -----
+
+	var commands = new Map<String, (args:Dynamic, onDone:Dynamic->Void) -> Void>();
+
+	/**
+		register a single command f.
+		`args` can be null, or an object that can be parse from/to json.
+		`onDone(result)` must be call when `f` finished, and `result` can be null or a json serializable object.
+	 */
+	public function registerCommand( name : String, f : (args:Dynamic, onDone:Dynamic->Void) -> Void ) {
+		commands.set(name, f);
+	}
+
+	/**
+		Register functions marked with `@cmd` in instance `o` as command handler (class of `o` needs `@:rtti` and `@:keep`).
+		This is done with `Reflect` and `registerCommand`, `onDone` call are inserted automatically when necessary.
+		Function name will be used as `cmd` key (and alias name if `@cmd("aliasname")`),
+		if multiple function use the same name, only the latest registered is taken into account.
+
+		Supported `@cmd` function signature:
+		``` haxe
+		@cmd function foo() : Dynamic {}
+		@cmd function foo(args : Dynamic) : Dynamic {}
+		@cmd function foo(onDone : Dynamic->Void) : Void {}
+		@cmd function foo(args : Dynamic, onDone : Dynamic->Void) : Void {}
+		```
+	 */
+	public function registerCommands( o : Dynamic ) {
+		function regRec( cl : Dynamic ) {
+			if( !haxe.rtti.Rtti.hasRtti(cl) )
+				return;
+			var rtti = haxe.rtti.Rtti.getRtti(cl);
+			for( field in rtti.fields ) {
+				var cmd = null;
+				for( m in field.meta ) {
+					if( m.name == "cmd" ) {
+						cmd = m;
+						break;
+					}
+				}
+				if( cmd != null ) {
+					switch( field.type ) {
+						case CFunction(args, ret):
+							var name = field.name;
+							var func = Reflect.field(o, field.name);
+							var f = null;
+							if( args.length == 0 ) {
+								f = (args, onDone) -> onDone(Reflect.callMethod(o, func, []));
+							} else if( args.length == 1 && args[0].t.match(CFunction(_,_))) {
+								f = (args, onDone) -> Reflect.callMethod(o, func, [onDone]);
+							} else if( args.length == 1 ) {
+								f = (args, onDone) -> onDone(Reflect.callMethod(o, func, [args]));
+							} else if( args.length == 2 && args[1].t.match(CFunction(_,_)) ) {
+								f = (args, onDone) -> Reflect.callMethod(o, func, [args, onDone]);
+							} else {
+								logError("Invalid @cmd, found: " + args);
+								continue;
+							}
+							registerCommand(name, f);
+							if( cmd.params.length == 1 ) {
+								var alias = StringTools.trim(StringTools.replace(cmd.params[0], "\"", ""));
+								registerCommand(alias, f);
+							}
+						default:
+					}
+				}
+			}
+		}
+		var cl = Type.getClass(o);
+		while( cl != null ) {
+			regRec(cl);
+			cl = Type.getSuperClass(cl);
+		}
+	}
+
+	@cmd("log") function logCmd( args : Dynamic ) {
+		log("[>] " + args);
+	}
+
+	@cmd function cwd() {
+		return Sys.getCwd();
+	}
+
+	@cmd function programPath() {
+		return Sys.programPath();
+	}
+
+#if hl
+	@cmd function dump( args : { file : String } ) {
+		hl.Gc.major();
+		hl.Gc.dumpMemory(args?.file);
+		if( hxd.res.Resource.LIVE_UPDATE ) {
+			var msg = "hxd.res.Resource.LIVE_UPDATE is on, you may want to disable it for mem dumps; RemoteConsole can also impact memdumps.";
+			logError(msg);
+			sendCommand("log", msg);
+		}
+	}
+
+	@cmd function prof( args : { action : String, samples : Int, delay_ms : Int }, onDone : Dynamic -> Void ) {
+		function doProf( args ) {
+			switch( args.action ) {
+			case "start":
+				hl.Profile.event(-7, "" + (args.samples > 0 ? args.samples : 10000)); // setup
+				hl.Profile.event(-3); // clear data
+				hl.Profile.event(-5); // resume all
+			case "resume":
+				hl.Profile.event(-5); // resume all
+			case "pause":
+				hl.Profile.event(-4); // pause all
+			case "dump":
+				hl.Profile.event(-6); // save dump
+				hl.Profile.event(-4); // pause all
+				hl.Profile.event(-3); // clear data
+			default:
+				sendCommand("log", "Missing argument action for prof");
+			}
+		}
+		if( args == null ) {
+			onDone(null);
+		} else if( args.delay_ms > 0 ) {
+			haxe.Timer.delay(function() {
+				doProf(args);
+				onDone(null);
+			}, args.delay_ms);
+		} else {
+			doProf(args);
+			onDone(null);
+		}
+	}
+#end
+}

+ 15 - 9
hxd/net/Socket.hx

@@ -41,7 +41,7 @@ class Socket {
 	public var out(default, null) : SocketOutput;
 	public var input(default, null) : SocketInput;
 	public var timeout(default, set) : Null<Float>;
-	
+
 	public static inline var ALLOW_BIND = #if (hl || (nodejs && hxnodejs)) true #else false #end;
 
 	public function new() {
@@ -68,6 +68,16 @@ class Socket {
 			input = new HLSocketInput(this);
 			onConnect();
 		});
+		#elseif (nodejs && hxnodejs)
+		s = js.node.Net.connect(port, host, function() {
+			out = new NodeSocketOutput(this);
+			input = new NodeSocketInput(this);
+			onConnect();
+		});
+		s.on('error', function() {
+			close();
+			onError("Connection closed");
+		});
 		#else
 		throw "Not implemented";
 		#end
@@ -98,6 +108,10 @@ class Socket {
 		js.node.Net.createServer(function(sock) {
 			var s = new Socket();
 			s.s = sock;
+			s.s.on('error', function(e) {
+				s.close();
+				s.onError("Connection closed");
+			});
 			s.out = new NodeSocketOutput(s);
 			s.input = new NodeSocketInput(s);
 			openedSocks.push(s);
@@ -243,17 +257,9 @@ class NodeSocketOutput extends SocketOutput {
 	public function new(s) {
 		super();
 		this.s = s;
-		@:privateAccess s.s.on('error', () -> writeResult(false));
 		@:privateAccess s.s.on('end', () -> s.close());
 	}
 
-	function writeResult(b) {
-		if( !b ) {
-			s.close();
-			s.onError("Failed to write data");
-		}
-	}
-
 	override function writeByte(c:Int) {
 		if( tmpBuf == null )
 			tmpBuf = js.node.Buffer.alloc(1);