RemoteConsole.hx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. package hrt.impl;
  2. typedef RemoteMenuAction = {
  3. name : String,
  4. ?cdbSheet : String,
  5. }
  6. /**
  7. A simple socket-based local communication channel (plaintext and unsafe),
  8. aim at communicate between 2 programs (e.g. Hide and a HL game).
  9. Usage in game (see also hrt.impl.RemoteTools):
  10. ```haxe
  11. var rcmd = new hrt.impl.RemoteConsole();
  12. // rcmd.log = (msg) -> logToUI(msg);
  13. // rcmd.logError = (msg) -> logErrorToUI(msg);
  14. rcmd.connect();
  15. rcmd.sendCommand("log", "Hello!", function(r) {});
  16. ```
  17. */
  18. class RemoteConsole {
  19. public static var DEFAULT_HOST : String = "127.0.0.1";
  20. public static var DEFAULT_PORT : Int = 40001;
  21. public static var SILENT_CONNECT : Bool = true;
  22. public var host : String;
  23. public var port : Int;
  24. var sock : hxd.net.Socket;
  25. public var connections : Array<RemoteConsoleConnection>;
  26. public function new( ?port : Int, ?host : String ) {
  27. this.host = host ?? DEFAULT_HOST;
  28. this.port = port ?? DEFAULT_PORT;
  29. this.connections = [];
  30. }
  31. public function startServer( ?onClient : RemoteConsoleConnection -> Void ) {
  32. close();
  33. sock = new hxd.net.Socket();
  34. sock.onError = function(msg) {
  35. logError("Socket Error: " + msg);
  36. close();
  37. }
  38. sock.bind(host, port, function(s) {
  39. var connection = new RemoteConsoleConnection(this, s);
  40. connections.push(connection);
  41. s.onError = function(msg) {
  42. connection.logError("Client error: " + msg);
  43. connection.close();
  44. connection = null;
  45. }
  46. s.onData = () -> connection.handleOnData();
  47. if( onClient != null )
  48. onClient(connection);
  49. connection.log("Client connected");
  50. }, 1);
  51. log('Server started at $host:$port');
  52. }
  53. public function connect( ?onConnected : Bool -> Void ) {
  54. close();
  55. sock = new hxd.net.Socket();
  56. var connection = new RemoteConsoleConnection(this, sock);
  57. connections.push(connection);
  58. sock.onError = function(msg) {
  59. if( !SILENT_CONNECT )
  60. logError("Socket Error: " + msg);
  61. close();
  62. if( onConnected != null )
  63. onConnected(false);
  64. }
  65. sock.onData = () -> connection.handleOnData();
  66. sock.connect(host, port, function() {
  67. log("Connected to server");
  68. if( onConnected != null )
  69. onConnected(true);
  70. });
  71. if( !SILENT_CONNECT )
  72. log('Connecting to $host:$port');
  73. }
  74. public function close() {
  75. if( sock != null ) {
  76. sock.close();
  77. sock = null;
  78. }
  79. // prevent remove during iteration by c.close
  80. var prevConnections = connections;
  81. connections = [];
  82. for( c in prevConnections ) {
  83. c.close();
  84. }
  85. onClose();
  86. }
  87. public function isConnected() {
  88. return sock != null;
  89. }
  90. public dynamic function onClose() {
  91. }
  92. public dynamic function log( msg : String ) {
  93. trace(msg);
  94. }
  95. public dynamic function logError( msg : String ) {
  96. trace('[Error] $msg');
  97. }
  98. public function sendCommand( cmd : String, ?args : Dynamic, ?onResult : Dynamic -> Void ) {
  99. if( connections.length == 0 ) {
  100. // Ignore send when not really connected
  101. } else if( connections.length == 1 ) {
  102. connections[0].sendCommand(cmd, args, onResult);
  103. } else {
  104. for( c in connections ) {
  105. c.sendCommand(cmd, args, onResult);
  106. }
  107. }
  108. }
  109. }
  110. @:keep
  111. @:rtti
  112. class RemoteConsoleConnection {
  113. var UID : Int = 0;
  114. var parent : RemoteConsole;
  115. var sock : hxd.net.Socket;
  116. var waitReply : Map<Int, Dynamic->Void> = [];
  117. var commands : Map<String, (args:Dynamic, onDone:Dynamic->Void) -> Void> = [];
  118. public function new( parent : RemoteConsole, s : hxd.net.Socket ) {
  119. this.parent = parent;
  120. this.sock = s;
  121. registerCommands(this);
  122. }
  123. public function close() {
  124. UID = 0;
  125. waitReply = [];
  126. commands = [];
  127. if( sock != null )
  128. sock.close();
  129. sock = null;
  130. if( parent != null )
  131. parent.connections.remove(this);
  132. parent = null;
  133. onClose();
  134. }
  135. public function isConnected() {
  136. return sock != null;
  137. }
  138. public dynamic function onClose() {
  139. }
  140. public dynamic function log( msg : String ) {
  141. trace(msg);
  142. }
  143. public dynamic function logError( msg : String ) {
  144. trace('[Error] $msg');
  145. }
  146. public function sendCommand( cmd : String, ?args : Dynamic, ?onResult : Dynamic -> Void ) {
  147. if( sock == null )
  148. return;
  149. var id = ++UID;
  150. waitReply.set(id, onResult);
  151. sendData(cmd, args, id);
  152. }
  153. function sendData( cmd : String, args : Dynamic, id : Int ) {
  154. var obj = { cmd : cmd, args : args, id : id};
  155. var bytes = haxe.io.Bytes.ofString(haxe.Json.stringify(obj) + "\n");
  156. sock.out.writeBytes(bytes, 0, bytes.length);
  157. }
  158. public function handleOnData() {
  159. while( sock.input.available > 0 ) {
  160. var str = sock.input.readLine().toString();
  161. var obj = try { haxe.Json.parse(str); } catch (e) { logError("Parse error: " + e); null; };
  162. if( obj == null || obj.id == null ) {
  163. continue;
  164. }
  165. var id : Int = obj.id;
  166. if( id <= 0 ) {
  167. var onResult = waitReply.get(-id);
  168. waitReply.remove(-id);
  169. if( onResult != null ) {
  170. onResult(obj.args);
  171. }
  172. } else {
  173. onCommand(obj.cmd, obj.args, (result) -> sendData(null, result, -id));
  174. }
  175. }
  176. }
  177. function onCommand( cmd : String, args : Dynamic, onDone : Dynamic -> Void ) : Void {
  178. if( cmd == null )
  179. return;
  180. var command = commands.get(cmd);
  181. if( command == null ) {
  182. logError("Unsupported command " + cmd);
  183. return;
  184. }
  185. command(args, onDone);
  186. }
  187. // ----- Commands -----
  188. /**
  189. Register a single command `name`, with `f` as command handler.
  190. `args` can be null, or an object that can be parsed from/to json.
  191. `onDone(result)` must be called when `f` finishes, and `result` can be null or a json serializable object.
  192. If `f` is `null`, the command is considered removed.
  193. */
  194. public function registerCommand( name : String, f : (args:Dynamic, onDone:Dynamic->Void) -> Void ) {
  195. commands.set(name, f);
  196. }
  197. /**
  198. Register functions marked with `@cmd` in instance `o` as command handler (class of `o` needs `@:rtti` and `@:keep`).
  199. This is done with `Reflect` and `registerCommand`, `onDone` call are inserted automatically when necessary.
  200. Function name will be used as `cmd` key (and alias name if `@cmd("aliasname")`),
  201. if multiple function use the same name, only the latest registered is taken into account.
  202. Supported `@cmd` function signature:
  203. ``` haxe
  204. @cmd function foo() : Dynamic {}
  205. @cmd function foo(args : Dynamic) : Dynamic {}
  206. @cmd function foo(onDone : Dynamic->Void) : Void {}
  207. @cmd function foo(args : Dynamic, onDone : Dynamic->Void) : Void {}
  208. ```
  209. */
  210. public function registerCommands( o : Dynamic ) {
  211. function regRec( cl : Dynamic ) {
  212. if( !haxe.rtti.Rtti.hasRtti(cl) )
  213. return;
  214. var rtti = haxe.rtti.Rtti.getRtti(cl);
  215. for( field in rtti.fields ) {
  216. var cmd = null;
  217. for( m in field.meta ) {
  218. if( m.name == "cmd" ) {
  219. cmd = m;
  220. break;
  221. }
  222. }
  223. if( cmd != null ) {
  224. switch( field.type ) {
  225. case CFunction(args, ret):
  226. var name = field.name;
  227. var func = Reflect.field(o, field.name);
  228. var f = null;
  229. if( args.length == 0 ) {
  230. f = (args, onDone) -> onDone(Reflect.callMethod(o, func, []));
  231. } else if( args.length == 1 && args[0].t.match(CFunction(_,_))) {
  232. f = (args, onDone) -> Reflect.callMethod(o, func, [onDone]);
  233. } else if( args.length == 1 ) {
  234. f = (args, onDone) -> onDone(Reflect.callMethod(o, func, [args]));
  235. } else if( args.length == 2 && args[1].t.match(CFunction(_,_)) ) {
  236. f = (args, onDone) -> Reflect.callMethod(o, func, [args, onDone]);
  237. } else {
  238. logError("Invalid @cmd, found: " + args);
  239. continue;
  240. }
  241. registerCommand(name, f);
  242. if( cmd.params.length == 1 ) {
  243. var alias = StringTools.trim(StringTools.replace(cmd.params[0], "\"", ""));
  244. registerCommand(alias, f);
  245. }
  246. default:
  247. }
  248. }
  249. }
  250. }
  251. var cl = Type.getClass(o);
  252. while( cl != null ) {
  253. regRec(cl);
  254. cl = Type.getSuperClass(cl);
  255. }
  256. }
  257. @cmd("log") function logCmd( args : Dynamic ) {
  258. log("[>] " + args);
  259. }
  260. @cmd("logError") function logErrorCmd( args : Dynamic ) {
  261. logError("[>] " + args);
  262. }
  263. function sendLog( msg : String ) {
  264. sendCommand("log", msg);
  265. }
  266. function sendLogError( msg : String ) {
  267. sendCommand("logError", msg);
  268. }
  269. @cmd function info() {
  270. return {
  271. programPath : Sys.programPath(),
  272. args : Sys.args(),
  273. cwd : Sys.getCwd(),
  274. };
  275. }
  276. // ----- Console ------
  277. @cmd function runInConsole( args : { cmd : String } ) : Int {
  278. return onConsoleCommand(args?.cmd ?? "");
  279. }
  280. public dynamic function onConsoleCommand( cmd : String ) : Int {
  281. sendLogError('onConsoleCommand not implemented, received $cmd');
  282. return -1;
  283. }
  284. public var menuActions(default, null) : Array<RemoteMenuAction> = null;
  285. @cmd function registerMenuActions( args : { actions : Array<RemoteMenuAction> } ) {
  286. menuActions = args?.actions;
  287. }
  288. @cmd function menuAction( args : { action : RemoteMenuAction, id : String } ) : Int {
  289. return onMenuAction(args?.action, args?.id);
  290. }
  291. public dynamic function onMenuAction( action : RemoteMenuAction, id : String ) : Int {
  292. sendLogError('onMenuAction not implemented');
  293. return -1;
  294. }
  295. #if editor
  296. // ----- Hide ------
  297. var parser : hscript.Parser;
  298. @cmd function open( args : { ?file : String, ?line : Int, ?column : Int, ?cdbsheet : String,
  299. ?selectExpr : String } ) {
  300. if( args == null )
  301. return;
  302. if( parser == null ) {
  303. parser = new hscript.Parser();
  304. parser.identChars += "$";
  305. }
  306. if( args.cdbsheet != null ) {
  307. var sheet = hide.Ide.inst.database.getSheet(args.cdbsheet);
  308. hide.Ide.inst.open("hide.view.CdbTable", {}, null, function(view) {
  309. hide.Ide.inst.focus();
  310. var line = args.line;
  311. if( sheet != null && args.selectExpr != null ) {
  312. try {
  313. var expr = parser.parseString(args.selectExpr);
  314. for( i in 0...sheet.lines.length ) {
  315. if( evalExpr(sheet.lines[i], expr) == true ) {
  316. line = i;
  317. break;
  318. }
  319. }
  320. } catch( e ) {
  321. hide.Ide.inst.quickError(e);
  322. }
  323. }
  324. Std.downcast(view, hide.view.CdbTable).goto(sheet, line, args.column ?? -1);
  325. });
  326. } else if( args.file != null ) {
  327. hide.Ide.inst.showFileInResources(args.file);
  328. hide.Ide.inst.openFile(args.file, null, function(view) {
  329. hide.Ide.inst.focus();
  330. var domkitView = Std.downcast(view, hide.view.Domkit);
  331. if( domkitView != null ) {
  332. var col = args.column ?? 0;
  333. var line = (args.line ?? 0) + 1;
  334. haxe.Timer.delay(function() {
  335. var cssEditor = @:privateAccess domkitView.cssEditor;
  336. if (cssEditor != null) {
  337. cssEditor.focus();
  338. @:privateAccess cssEditor.editor.revealLineInCenter(line);
  339. @:privateAccess cssEditor.editor.setPosition({ column: col, lineNumber: line });
  340. }
  341. }, 1);
  342. }
  343. if( args.selectExpr != null ) {
  344. var sceneEditor : hide.comp.SceneEditor = null;
  345. var prefabView = Std.downcast(view, hide.view.Prefab);
  346. if( prefabView != null ) {
  347. sceneEditor = prefabView.sceneEditor;
  348. }
  349. var fxView = Std.downcast(view, hide.view.FXEditor);
  350. if( fxView != null ) {
  351. @:privateAccess sceneEditor = fxView.sceneEditor;
  352. }
  353. var modelView = Std.downcast(view, hide.view.Model);
  354. if( modelView != null ) {
  355. @:privateAccess sceneEditor = modelView.sceneEditor;
  356. }
  357. if( sceneEditor != null ) {
  358. try {
  359. var expr = parser.parseString(args.selectExpr);
  360. @:privateAccess var objs = sceneEditor.sceneData.findAll(null, function(o) {
  361. return evalExpr(o, expr);
  362. });
  363. sceneEditor.delayReady(() -> sceneEditor.selectElements(objs));
  364. } catch( e ) {
  365. hide.Ide.inst.quickError(e);
  366. }
  367. }
  368. }
  369. });
  370. }
  371. }
  372. function evalExpr( o : Dynamic, e : hscript.Expr ) : Dynamic {
  373. switch( e.e ) {
  374. case EConst(c):
  375. switch( c ) {
  376. case CInt(v): return v;
  377. case CFloat(f): return f;
  378. case CString(s): return s;
  379. }
  380. case EIdent("$"):
  381. return o;
  382. case EIdent("null"):
  383. return null;
  384. case EIdent(v):
  385. return v; // Unknown ident, consider as a String literal
  386. case EField(e, f):
  387. var v = evalExpr(o, e);
  388. return Reflect.field(v, f);
  389. case EBinop(op, e1, e2):
  390. var v1 = evalExpr(o, e1);
  391. var v2 = evalExpr(o, e2);
  392. switch( op ) {
  393. case "==": return Reflect.compare(v1, v2) == 0;
  394. case "&&": return v1 == true && v2 == true;
  395. default:
  396. throw "Can't eval " + Std.string(v1) + " " + op + " " + Std.string(v2);
  397. }
  398. default:
  399. throw "Unsupported expression " + hscript.Printer.toString(e);
  400. }
  401. }
  402. #end
  403. #if hl
  404. // ----- Hashlink ------
  405. @cmd function gcMajor() : Int {
  406. var start = haxe.Timer.stamp();
  407. hl.Gc.major();
  408. var duration_us = (haxe.Timer.stamp() - start) * 1_000_000.;
  409. return Std.int(duration_us);
  410. }
  411. @cmd function dumpMemory( args : { file : String } ) {
  412. hl.Gc.major();
  413. hl.Gc.dumpMemory(args?.file);
  414. if( hxd.res.Resource.LIVE_UPDATE ) {
  415. var msg = "hxd.res.Resource.LIVE_UPDATE is on, you may want to disable it for mem dumps; RemoteConsole can also impact memdumps.";
  416. logError(msg);
  417. sendLogError(msg);
  418. }
  419. }
  420. @cmd function liveObjects( args : { clname : String } ) : Int {
  421. if( args == null || args.clname == null )
  422. return -1;
  423. #if( hl_ver >= version("1.15.0") && haxe_ver >= 5 )
  424. hl.Gc.major();
  425. var cl = std.Type.resolveClass(args.clname);
  426. if( cl == null ) {
  427. sendLogError('Failed to find class for ${args.clname}');
  428. return -1;
  429. }
  430. var c = hl.Gc.getLiveObjects(cl, 0);
  431. return c.count;
  432. #else
  433. sendLogError("getLiveObjects not supported, please use hl >= 1.15.0 and haxe >= 5.0.0");
  434. return -1;
  435. #end
  436. }
  437. @cmd function profCpu( args : { action : String, samples : Int, delay_ms : Int }, onDone : Dynamic -> Void ) {
  438. function doProf( args ) {
  439. switch( args.action ) {
  440. case "start":
  441. hl.Profile.event(-7, "" + (args.samples > 0 ? args.samples : 10000)); // setup
  442. hl.Profile.event(-3); // clear data
  443. hl.Profile.event(-5); // resume all
  444. case "resume":
  445. hl.Profile.event(-5); // resume all
  446. case "pause":
  447. hl.Profile.event(-4); // pause all
  448. case "dump":
  449. hl.Profile.event(-6); // save dump
  450. hl.Profile.event(-4); // pause all
  451. hl.Profile.event(-3); // clear data
  452. default:
  453. sendLogError('profCpu: action ${args?.action} not supported');
  454. }
  455. }
  456. if( args == null ) {
  457. onDone(null);
  458. } else if( args.delay_ms > 0 ) {
  459. haxe.Timer.delay(function() {
  460. doProf(args);
  461. onDone(null);
  462. }, args.delay_ms);
  463. } else {
  464. doProf(args);
  465. onDone(null);
  466. }
  467. }
  468. @cmd function profTrack( args : { action : String } ) : Int {
  469. switch( args?.action ) {
  470. case "start":
  471. var tmp = hl.Profile.globalBits;
  472. tmp.set(Alloc);
  473. hl.Profile.globalBits = tmp;
  474. hl.Profile.reset();
  475. case "dump":
  476. hl.Profile.dump("memprofSize.dump", true, false);
  477. hl.Profile.dump("memprofCount.dump", false, true);
  478. default:
  479. sendLogError('Action ${args?.action} not supported');
  480. return -1;
  481. }
  482. return 0;
  483. }
  484. // ----- Heaps ------
  485. @cmd function dumpGpu( args : { action : String } ) : Int {
  486. switch( args?.action ) {
  487. case "enable":
  488. h3d.impl.MemoryManager.enableTrackAlloc(true);
  489. case "disable":
  490. h3d.impl.MemoryManager.enableTrackAlloc(false);
  491. case "dump":
  492. var engine = h3d.Engine.getCurrent();
  493. if( engine == null ) {
  494. sendLogError("h3d.Engine.getCurrent() == null");
  495. return -1;
  496. }
  497. var stats = engine.mem.allocStats();
  498. if( stats.length <= 0 ) {
  499. var msg = "No alloc found, enable with h3d.impl.MemoryManager.enableTrackAlloc()";
  500. sendLogError(msg);
  501. return -2;
  502. }
  503. var sb = new StringBuf();
  504. stats.sort((s1, s2) -> (s1.size > s2.size && s2.size > 0) ? -1 : 1);
  505. var total = 0;
  506. var textureSize = 0;
  507. var bufferSize = 0;
  508. for( s in stats ) {
  509. var size = Std.int(s.size / 1024);
  510. total += size;
  511. if ( s.tex )
  512. textureSize += size;
  513. else
  514. bufferSize += size;
  515. sb.add((s.tex?"Texture ":"Buffer ") + '${s.position} #${s.count} ${Std.int(s.size/1024)}kb\n');
  516. }
  517. sb.add('TOTAL: ${total}kb\n');
  518. sb.add('TEXTURE TOTAL: ${textureSize}kb\n');
  519. sb.add('BUFFER TOTAL: ${bufferSize}kb\n');
  520. sb.add('\nDETAILS\n');
  521. for(s in stats) {
  522. sb.add('${s.position} #${s.count} ${Std.int(s.size/1024)}kb\n');
  523. s.stacks.sort((s1, s2) -> (s1.size > s2.size && s2.size > 0) ? -1 : 1);
  524. for (stack in s.stacks) {
  525. sb.add('\t#${stack.count} ${Std.int(stack.size/1024)}kb ${stack.stack.split('\n').join('\n\t\t')}\n');
  526. for ( s in stack.stats )
  527. sb.add('\t\t${s.name} ${Std.int(s.size/1024)}kb\n');
  528. }
  529. }
  530. sys.io.File.saveContent("gpudump.txt", sb.toString());
  531. default:
  532. sendLogError('Action ${args?.action} not supported');
  533. return -1;
  534. }
  535. return 0;
  536. }
  537. @cmd function profScene( args : { action : String } ) : Int {
  538. #if sceneprof
  539. switch( args?.action ) {
  540. case "start":
  541. h3d.impl.SceneProf.start();
  542. case "dump":
  543. h3d.impl.SceneProf.stop();
  544. h3d.impl.SceneProf.save("sceneprof.json");
  545. default:
  546. sendLogError('Action ${args?.action} not supported');
  547. return -1;
  548. }
  549. return 0;
  550. #else
  551. sendLogError("SceneProf not supported, please compile with -D sceneprof");
  552. return -1;
  553. #end
  554. }
  555. @cmd function buildFiles( onDone : Int -> Void ) {
  556. sendLog("Build files begin");
  557. BuildTools.buildAllFiles( null, null, null, function(count, errCount) {
  558. if( errCount > 0 ) {
  559. sendLogError('Build files has $errCount errors, please check game log for more details');
  560. }
  561. onDone(count);
  562. });
  563. }
  564. #end
  565. }