From 511e7f2981fbdab5eefe2fadec3080c48c4968a5 Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Wed, 30 Oct 2024 00:16:52 +0100 Subject: [PATCH 01/11] fix: Ensure previous hl process is killed --- src/watch/Watch.hx | 606 +++++++++++++++++++++++++-------------------- 1 file changed, 337 insertions(+), 269 deletions(-) diff --git a/src/watch/Watch.hx b/src/watch/Watch.hx index a2756e9..7fb3aa6 100644 --- a/src/watch/Watch.hx +++ b/src/watch/Watch.hx @@ -6,65 +6,70 @@ import eval.luv.FsEvent; import eval.luv.Process; import eval.luv.SockAddr; import eval.luv.Tcp; +import eval.luv.Result; +import eval.luv.Idle; +import eval.luv.Dir; +import haxe.ds.Option; import haxe.macro.Context; import haxe.io.Path; import sys.FileSystem; + using StringTools; using Lambda; private final loop = sys.thread.Thread.current().events; private function fail(message: String, ?error: Dynamic) { - Sys.println(message); - if (error != null) - Sys.print('$error'); - Sys.exit(1); + Sys.println(message); + if (error != null) + Sys.print('$error'); + Sys.exit(1); } private final noInputOptions = [ - 'interp', 'haxelib-global', 'no-traces', 'no-output', 'no-inline', 'no-opt', - 'v', 'verbose', 'debug', 'prompt', 'times', 'next', 'each', 'flash-strict' -]; + 'interp', 'haxelib-global', 'no-traces', 'no-output', 'no-inline', 'no-opt', + 'v', 'verbose', 'debug', 'prompt', 'times', 'next', 'each', 'flash-strict' + ]; private final outputs = ['php', 'cpp', 'cs', 'java']; function buildArguments(args: Array): BuildConfig { - final arguments = []; - final excludes = []; - final includes = []; - final forward = []; - final dist = []; - var i = 0; - function skip() i++; - while (i < args.length) { - switch [args[i], args[i + 1]] { - case - ['-L' | '-lib' | '--library', 'watch'], - ['--macro', 'watch.Watch.register()']: - skip(); - case ['-D' | '--define', define] if (define.startsWith('watch.exclude')): - excludes.push(define.substr(define.indexOf('=') + 1)); - skip(); - case ['-D' | '--define', define] if (define.startsWith('watch.include')): - includes.push(define.substr(define.indexOf('=') + 1)); + final arguments = []; + final excludes = []; + final includes = []; + final forward = []; + final dist = []; + var i = 0; + function skip() i++; + while (i < args.length) { + switch [args[i], args[i + 1]] { + case + ['-L' | '-lib' | '--library', 'watch'], + ['--macro', 'watch.Watch.register()']: + skip(); + case ['-D' | '--define', define] if (define.startsWith('watch.exclude')): + excludes.push(define.substr(define.indexOf('=') + 1)); + skip(); + case ['-D' | '--define', define] if (define.startsWith('watch.include')): + includes.push(define.substr(define.indexOf('=') + 1)); + skip(); + case [arg, next]: + final option = arg.startsWith('--') ? arg.substr(2) : arg.substr(1); + if (outputs.indexOf(option) > -1) + dist.push(next); + forward.push(args[i]); + } skip(); - case [arg, next]: - final option = arg.startsWith('--') ? arg.substr(2) : arg.substr(1); - if (outputs.indexOf(option) > -1) - dist.push(next); - forward.push(args[i]); } - skip(); - } - var inputExpected = false; - for (arg in forward) { - final isOption = arg.startsWith('-'); + var inputExpected = false; + for (arg in forward) { + final isOption = arg.startsWith('-'); if (inputExpected && !isOption) arguments[arguments.length - 1] += ' $arg'; else arguments.push(arg); - final option = arg.startsWith('--') ? arg.substr(2) : arg.substr(1); + final option = arg.startsWith('--') ? arg.substr(2) : arg.substr(1); inputExpected = isOption && noInputOptions.indexOf(option) == -1; - } + } return {arguments: arguments, excludes: excludes, includes: includes, dist: dist} } @@ -76,32 +81,32 @@ typedef BuildConfig = { } function isSubOf(path: String, parent: String) { - var a = Path.normalize(path); - var b = Path.normalize(parent); - final caseInsensitive = Sys.systemName() == 'Windows'; - if (caseInsensitive) { - a = a.toLowerCase(); - b = b.toLowerCase(); - } - return a.startsWith(b + '/'); + var a = Path.normalize(path); + var b = Path.normalize(parent); + final caseInsensitive = Sys.systemName() == 'Windows'; + if (caseInsensitive) { + a = a.toLowerCase(); + b = b.toLowerCase(); + } + return a.startsWith(b + '/'); } function pathIsIn(path: String, candidates: Array) { - return candidates.exists(parent -> path == parent || isSubOf(path, parent)); + return candidates.exists(parent -> path == parent || isSubOf(path, parent)); } function dedupePaths(paths: Array) { - final res = []; - final todo = paths.slice(0); - todo.sort((a, b) -> { - return b.length - a.length; - }); + final res = []; + final todo = paths.slice(0); + todo.sort((a, b) -> { + return b.length - a.length; + }); for (i in 0 ...todo.length) { - final path = todo[i]; - final isSubOfNext = pathIsIn(path, todo.slice(i + 1)); - if (!isSubOfNext) res.push(path); - } - return res; + final path = todo[i]; + final isSubOfNext = pathIsIn(path, todo.slice(i + 1)); + if (!isSubOfNext) res.push(path); + } + return res; } typedef Server = { @@ -110,247 +115,310 @@ typedef Server = { } function createServer(port: Int, cb: (server: Server) -> Void) { - if (Context.defined('watch.connect')) - return cb({ - build: (config, done) -> createBuild(port, config, done), - close: (done) -> done() - }); - final stdout = Process.inheritFd(Process.stdout, Process.stdout); - final stderr = Process.inheritFd(Process.stderr, Process.stderr); - function start(extension = '') { - switch Process.spawn(loop, 'haxe' + extension, ['haxe', '--wait', '$port'], { - redirect: [stdout, stderr], - onExit: (_, exitStatus, _) -> fail('Completion server exited', exitStatus) - }) { - case Ok(process): - cb({ - build: (config, done) -> { - createBuild(port, config, done); - }, - close: (done) -> process.close(done) + if (Context.defined('watch.connect')) + return cb({ + build: (config, done) -> createBuild(port, config, done), + close: (done) -> done() }); - case Error(UV_ENOENT) if (Sys.systemName() == 'Windows' && extension == ''): - start('.cmd'); - case Error(e): fail('Could not start completion server, is haxe in path?', e); + final stdout = Process.inheritFd(Process.stdout, Process.stdout); + final stderr = Process.inheritFd(Process.stderr, Process.stderr); + function start(extension = '') { + switch Process.spawn(loop, 'haxe' + extension, ['haxe', '--wait', '$port'], { + redirect: [stdout, stderr], + onExit: (_, exitStatus, _) -> fail('Completion server exited', exitStatus) + }) { + case Ok(process): + cb({ + build: (config, done) -> { + createBuild(port, config, done); + }, + close: (done) -> process.close(done) + }); + case Error(UV_ENOENT) if (Sys.systemName() == 'Windows' && extension == ''): + start('.cmd'); + case Error(e): fail('Could not start completion server, is haxe in path?', e); + } + } + switch [SockAddr.ipv4('127.0.0.1', port), Tcp.init(loop)] { + case [Ok(addr), Ok(socket)]: + socket.connect(addr, res -> switch res { + case Ok(_): + socket.close(() -> { + createServer(port + 1, cb); + }); + case Error(_): + socket.close(() -> start()); + }); + case [_, Error(e)] | [Error(e), _]: + fail('Could not check if port is open', e); } - } - switch [SockAddr.ipv4('127.0.0.1', port), Tcp.init(loop)] { - case [Ok(addr), Ok(socket)]: - socket.connect(addr, res -> switch res { - case Ok(_): - socket.close(() -> { - createServer(port + 1, cb); - }); - case Error(_): - socket.close(() -> start()); - }); - case [_, Error(e)] | [Error(e), _]: - fail('Could not check if port is open', e); - } } private function shellOut(command: String) { - return switch Sys.systemName() { - case 'Windows': ['cmd.exe', '/c', command]; - default: ['sh', '-c', command]; - } -} + return switch Sys.systemName() { + case 'Windows': ['cmd.exe', '/c', command]; + default: ['sh', '-c', command]; + } +} function runCommand(command: String) { - final args = shellOut(command).map(NativeString.fromString); - final stdout = Process.inheritFd(Process.stdout, Process.stdout); - final stderr = Process.inheritFd(Process.stderr, Process.stderr); - var exited = false; - return switch Process.spawn(loop, args[0], args, { - redirect: [stdout, stderr], - onExit: (_, _, _) -> exited = true - }) { - case Ok(process): cb -> { - // process.kill results in "Uncaught exception Cannot call null" - if (!exited) - switch Process.killPid(process.pid(), SIGKILL) { - case Ok(_): - case Error(e): fail('Could not end run command', e); - } - process.close(cb); + final args = shellOut(command).map(NativeString.fromString); + final stdout = Process.inheritFd(Process.stdout, Process.stdout); + final stderr = Process.inheritFd(Process.stderr, Process.stderr); + var exited = false; + return switch Process.spawn(loop, args[0], args, { + redirect: [stdout, stderr], + onExit: (_, _, _) -> exited = true + }) { + case Ok(process): cb -> { + if (!exited) { + final pid = process.pid(); + final tree = [pid => []]; + final pidsToProcess = [pid => true]; + buildProcessTree(pid, tree, pidsToProcess, parentPid -> { + // Get processes with parent pid + final psargs = '-o pid --no-headers --ppid $parentPid'; + final ps = new sys.io.Process('ps $psargs'); + ps; + }, () -> { + killAll(tree, _ -> cb()); + }); + } + process.close(cb); + } + case Error(e): + Sys.stderr().writeString('Could not run "$command", because $e'); + cb -> cb(); } - case Error(e): - Sys.stderr().writeString('Could not run "$command", because $e'); - cb -> cb(); - } } -function createBuild(port: Int, config: BuildConfig, done: (hasError: Bool) -> Void, retry = 0) { - if (retry > 1000) fail('Could not connect to port $port'); - switch [ - SockAddr.ipv4('127.0.0.1', port), - Tcp.init(loop) - ] { - case [Ok(addr), Ok(socket)]: - socket.connect(addr, res -> - switch res { - case Ok(_): - var hasError = false; - socket.readStart(res -> switch res { - case Ok(_.toString() => data): - for (line in data.split('\n')) { - switch (line.charCodeAt(0)) { - case 0x01: - Sys.print(line.substr(1).split('\x01').join('\n')); - case 0x02: - hasError = true; - default: - if (line.length > 0) { - Sys.stderr().writeString(line + '\n'); - Sys.stderr().flush(); - } - } - } - case Error(UV_EOF): - socket.close(() -> done(hasError)); - case Error(e): - fail('Server closed', e); - }); - socket.write([config.arguments.join('\n') + '\000'], (res, bytesWritten) -> switch res { - case Ok(_): - case Error(e): fail('Could not write to server', e); - }); - case Error(UV_ECONNREFUSED): - socket.close(() -> createBuild(port, config, done, retry + 1)); - case Error(e): - fail('Could not connect to server', e); +function buildProcessTree(parentPid:Int, tree:Map>, pidsToProcess:Map, getChildPpid:(pid:Int) -> sys.io.Process, cb:() -> Void) { + final result = getChildPpid(parentPid); + if (result.exitCode() == 0) { + pidsToProcess.remove(parentPid); + final pid = Std.parseInt(result.stdout.readAll().toString()); + final children = tree.get(parentPid) ?? []; + if (!children.has(pid)) { + children.push(pid); } - ); - case [_, Error(e)] | [Error(e), _]: - fail('Could not connect to server', e); - } -} + tree.set(parentPid, children); + tree.set(pid, []); + pidsToProcess.set(pid, true); + buildProcessTree(pid, tree, pidsToProcess, getChildPpid, cb); + } else { + pidsToProcess.remove(parentPid); + cb(); + } -function formatDuration(duration: Float) { - if (duration < 1000) - return '${Math.round(duration)}ms'; - final precision = 100; - final s = Math.round(duration / 1000 * precision) / precision; - return '${s}s'; + result.close(); } -function getFreePort(done: (port: haxe.ds.Option) -> Void) { - return switch [ - SockAddr.ipv4('127.0.0.1', 0), - Tcp.init(loop) - ] { - case [Ok(addr), Ok(socket)]: - switch socket.bind(addr) { - case Ok(_): - switch socket.getSockName() { - case Ok(addr): - socket.close(() -> done(Some(addr.port))); - default: - socket.close(() -> done(None)); - } - default: done(None); - } - default: done(None); - } +function killAll(tree:Map>, callback:(error:Option) -> Void) { + final killed:Map = []; + try { + [for (k in tree.keys()) k].iter(pid -> { + tree.get(pid).iter(pidpid -> { + final isKilled = killed.get(pidpid) ?? false; + if (!isKilled) { + switch Process.killPid(pidpid, SIGKILL) { + case Ok(_): + killed.set(pidpid, true); + case Error(e): + } + } + }); + if (!(killed.get(pid) ?? false)) { + switch Process.killPid(pid, SIGKILL) { + case Ok(_): + killed.set(pid, true); + case Error(e): + } + } + }); + if (callback != null) { + return callback(None); + } + } catch (err) { + if (callback != null) { + return callback(Some(err.toString())); + } else { + throw err; + } + } } -function register() { - function getPort(done: (port: Int) -> Void) { - switch Context.definedValue('watch.port') { - case null: - getFreePort(res -> +function createBuild(port: Int, config: BuildConfig, done: (hasError: Bool) -> Void, retry = 0) { + if (retry > 1000) fail('Could not connect to port $port'); + switch [ + SockAddr.ipv4('127.0.0.1', port), + Tcp.init(loop) + ] { + case [Ok(addr), Ok(socket)]: + socket.connect(addr, res -> switch res { - case Some(port): done(port); - default: fail('Could not find free port'); + case Ok(_): + var hasError = false; + socket.readStart(res -> switch res { + case Ok(_.toString() => data): + for (line in data.split('\n')) { + switch (line.charCodeAt(0)) { + case 0x01: + Sys.print(line.substr(1).split('\x01').join('\n')); + case 0x02: + hasError = true; + default: + if (line.length > 0) { + Sys.stderr().writeString(line + '\n'); + Sys.stderr().flush(); + } + } + } + case Error(UV_EOF): + socket.close(() -> done(hasError)); + case Error(e): + fail('Server closed', e); + }); + socket.write([config.arguments.join('\n') + '\000'], (res, bytesWritten) -> switch res { + case Ok(_): + case Error(e): fail('Could not write to server', e); + }); + case Error(UV_ECONNREFUSED): + socket.close(() -> createBuild(port, config, done, retry + 1)); + case Error(e): + fail('Could not connect to server', e); } ); - case v: done(Std.parseInt(v)); + case [_, Error(e)] | [Error(e), _]: + fail('Could not connect to server', e); } } - getPort(port -> { - final config = buildArguments(Sys.args()); - final excludes = config.excludes.map(FileSystem.absolutePath); - final includes = config.includes.map(FileSystem.absolutePath); - final classPaths = - Context.getClassPath().map(FileSystem.absolutePath) - .filter(path -> { - final isRoot = path == FileSystem.absolutePath('.'); - if (Context.defined('watch.excludeRoot') && isRoot) return false; - return !excludes.contains(path); - }); - final paths = dedupePaths(classPaths.concat(includes)); - createServer(port, server -> { - var next: Timer; - var building = false; - var closeRun = cb -> cb(); - function build() { - switch Timer.init(loop) { - case Ok(timer): - if (next != null) { - next.stop(); + + function formatDuration(duration: Float) { + if (duration < 1000) + return '${Math.round(duration)}ms'; + final precision = 100; + final s = Math.round(duration / 1000 * precision) / precision; + return '${s}s'; + } + + function getFreePort(done: (port: haxe.ds.Option) -> Void) { + return switch [ + SockAddr.ipv4('127.0.0.1', 0), + Tcp.init(loop) + ] { + case [Ok(addr), Ok(socket)]: + switch socket.bind(addr) { + case Ok(_): + switch socket.getSockName() { + case Ok(addr): + socket.close(() -> done(Some(addr.port))); + default: + socket.close(() -> done(None)); + } + default: done(None); + } + default: done(None); + } + } + + function register() { + function getPort(done: (port: Int) -> Void) { + switch Context.definedValue('watch.port') { + case null: + getFreePort(res -> + switch res { + case Some(port): done(port); + default: fail('Could not find free port'); } - next = timer; - timer.start(() -> { - if (building) { - build(); - return; + ); + case v: done(Std.parseInt(v)); + } + } + getPort(port -> { + final config = buildArguments(Sys.args()); + final excludes = config.excludes.map(FileSystem.absolutePath); + final includes = config.includes.map(FileSystem.absolutePath); + final classPaths = + Context.getClassPath().map(FileSystem.absolutePath) + .filter(path -> { + final isRoot = path == FileSystem.absolutePath('.'); + if (Context.defined('watch.excludeRoot') && isRoot) return false; + return !excludes.contains(path); + }); + final paths = dedupePaths(classPaths.concat(includes)); + createServer(port, server -> { + var next: Timer; + var building = false; + var closeRun = cb -> cb(); + function build() { + switch Timer.init(loop) { + case Ok(timer): + if (next != null) { + next.stop(); } - building = true; - final start = Sys.time(); - if (Context.defined('watch.verbose')) - Sys.println('\x1b[32m> Build started\x1b[39m'); - server.build(config, (hasError: Bool) -> { - building = false; - final duration = (Sys.time() - start) * 1000; - closeRun(() -> { - closeRun = cb -> cb(); - timer.close(() -> { - if (Context.defined('watch.verbose')) { - final status = if (hasError) 31 else 32; - Sys.println('\x1b[${status}m> Build finished\x1b[39m'); - } - if (hasError) { - Sys.println('\x1b[90m> Found errors\x1b[39m'); - } else { - Sys.println('\x1b[36m> Build completed in ${formatDuration(duration)}\x1b[39m'); - switch Context.definedValue('watch.run') { - case null: - case v: closeRun = runCommand(v); + next = timer; + timer.start(() -> { + if (building) { + build(); + return; + } + building = true; + final start = Sys.time(); + if (Context.defined('watch.verbose')) + Sys.println('\x1b[32m> Build started\x1b[39m'); + server.build(config, (hasError: Bool) -> { + building = false; + final duration = (Sys.time() - start) * 1000; + closeRun(() -> { + closeRun = cb -> cb(); + timer.close(() -> { + if (Context.defined('watch.verbose')) { + final status = if (hasError) 31 else 32; + Sys.println('\x1b[${status}m> Build finished\x1b[39m'); } - } + if (hasError) { + Sys.println('\x1b[90m> Found errors\x1b[39m'); + } else { + Sys.println('\x1b[36m> Build completed in ${formatDuration(duration)}\x1b[39m'); + switch Context.definedValue('watch.run') { + case null: + case v: closeRun = runCommand(v); + } + } + }); }); }); - }); - }, 100); - case Error(e): fail('Could not init time', e); + }, 100); + case Error(e): fail('Could not init time', e); + } } - } - function watch() { - for (path in paths) { - switch FsEvent.init(loop) { - case Ok(watcher): - watcher.start(path, [ - FsEventFlag.FS_EVENT_RECURSIVE - ], res -> - switch res { - case Ok({file: (_.toString()) => file}): - if (StringTools.endsWith(file, '.hx')) { - for (exclude in excludes) { - if (isSubOf(FileSystem.absolutePath(file), exclude)) - return; + function watch() { + for (path in paths) { + switch FsEvent.init(loop) { + case Ok(watcher): + watcher.start(path, [ + FsEventFlag.FS_EVENT_RECURSIVE + ], res -> + switch res { + case Ok({file: (_.toString()) => file}): + if (StringTools.endsWith(file, '.hx')) { + for (exclude in excludes) { + if (isSubOf(FileSystem.absolutePath(file), exclude)) + return; + } + build(); } - build(); - } - case Error(e): - } - ); - case Error(e): fail('Could not watch $path', e); + case Error(e): + } + ); + case Error(e): fail('Could not watch $path', e); + } } } - } - build(); - watch(); + build(); + watch(); + }); }); - }); - loop.loop(); -} + loop.loop(); + } + \ No newline at end of file From 515776712942aeda6337f82fd9be64098132d798 Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Wed, 30 Oct 2024 00:28:55 +0100 Subject: [PATCH 02/11] fix: Manually iterate over directory tree since recursive flag doesn't work on linux --- src/watch/Watch.hx | 337 +++++++++++++++++++++++++-------------------- 1 file changed, 185 insertions(+), 152 deletions(-) diff --git a/src/watch/Watch.hx b/src/watch/Watch.hx index 7fb3aa6..9fcc577 100644 --- a/src/watch/Watch.hx +++ b/src/watch/Watch.hx @@ -250,175 +250,208 @@ function killAll(tree:Map>, callback:(error:Option) -> V function createBuild(port: Int, config: BuildConfig, done: (hasError: Bool) -> Void, retry = 0) { if (retry > 1000) fail('Could not connect to port $port'); switch [ - SockAddr.ipv4('127.0.0.1', port), - Tcp.init(loop) + SockAddr.ipv4('127.0.0.1', port), + Tcp.init(loop) ] { - case [Ok(addr), Ok(socket)]: - socket.connect(addr, res -> - switch res { - case Ok(_): - var hasError = false; - socket.readStart(res -> switch res { - case Ok(_.toString() => data): - for (line in data.split('\n')) { - switch (line.charCodeAt(0)) { - case 0x01: - Sys.print(line.substr(1).split('\x01').join('\n')); - case 0x02: - hasError = true; - default: - if (line.length > 0) { - Sys.stderr().writeString(line + '\n'); - Sys.stderr().flush(); - } - } - } - case Error(UV_EOF): - socket.close(() -> done(hasError)); - case Error(e): - fail('Server closed', e); - }); - socket.write([config.arguments.join('\n') + '\000'], (res, bytesWritten) -> switch res { + case [Ok(addr), Ok(socket)]: + socket.connect(addr, res -> + switch res { case Ok(_): - case Error(e): fail('Could not write to server', e); - }); - case Error(UV_ECONNREFUSED): - socket.close(() -> createBuild(port, config, done, retry + 1)); - case Error(e): - fail('Could not connect to server', e); - } - ); - case [_, Error(e)] | [Error(e), _]: - fail('Could not connect to server', e); + var hasError = false; + socket.readStart(res -> switch res { + case Ok(_.toString() => data): + for (line in data.split('\n')) { + switch (line.charCodeAt(0)) { + case 0x01: + Sys.print(line.substr(1).split('\x01').join('\n')); + case 0x02: + hasError = true; + default: + if (line.length > 0) { + Sys.stderr().writeString(line + '\n'); + Sys.stderr().flush(); + } + } + } + case Error(UV_EOF): + socket.close(() -> done(hasError)); + case Error(e): + fail('Server closed', e); + }); + socket.write([config.arguments.join('\n') + '\000'], (res, bytesWritten) -> switch res { + case Ok(_): + case Error(e): fail('Could not write to server', e); + }); + case Error(UV_ECONNREFUSED): + socket.close(() -> createBuild(port, config, done, retry + 1)); + case Error(e): + fail('Could not connect to server', e); + } + ); + case [_, Error(e)] | [Error(e), _]: + fail('Could not connect to server', e); } - } - - function formatDuration(duration: Float) { +} + +function formatDuration(duration: Float) { if (duration < 1000) - return '${Math.round(duration)}ms'; + return '${Math.round(duration)}ms'; final precision = 100; final s = Math.round(duration / 1000 * precision) / precision; return '${s}s'; - } - - function getFreePort(done: (port: haxe.ds.Option) -> Void) { +} + +function getFreePort(done: (port: haxe.ds.Option) -> Void) { return switch [ - SockAddr.ipv4('127.0.0.1', 0), - Tcp.init(loop) + SockAddr.ipv4('127.0.0.1', 0), + Tcp.init(loop) ] { - case [Ok(addr), Ok(socket)]: - switch socket.bind(addr) { - case Ok(_): - switch socket.getSockName() { - case Ok(addr): - socket.close(() -> done(Some(addr.port))); - default: - socket.close(() -> done(None)); + case [Ok(addr), Ok(socket)]: + switch socket.bind(addr) { + case Ok(_): + switch socket.getSockName() { + case Ok(addr): + socket.close(() -> done(Some(addr.port))); + default: + socket.close(() -> done(None)); + } + default: done(None); } - default: done(None); - } - default: done(None); + default: done(None); } - } - - function register() { +} + +function childDirs(path:String, dirs:Array, cb:(dirs:Array, done:Bool) -> Void) { + final numDirs = dirs.length; + Dir.scan(loop, path, result -> { + switch result { + case Ok(dirScan): + var dirent:Dirent = dirScan.next(); + while (dirent != null) { + if (dirent.kind == DirentKind.DIR) { + final d = '${path}/${dirent.name.toString()}'; + dirs.push(d); + childDirs(d, dirs, cb); + } + dirent = dirScan.next(); + } + cb(dirs, dirs.length == numDirs); + case Error(e): + fail('Could not read child dir of $path', e); + } + }); +} + +function register() { function getPort(done: (port: Int) -> Void) { - switch Context.definedValue('watch.port') { - case null: - getFreePort(res -> - switch res { - case Some(port): done(port); - default: fail('Could not find free port'); - } - ); - case v: done(Std.parseInt(v)); - } + switch Context.definedValue('watch.port') { + case null: + getFreePort(res -> + switch res { + case Some(port): done(port); + default: fail('Could not find free port'); + } + ); + case v: done(Std.parseInt(v)); + } } getPort(port -> { - final config = buildArguments(Sys.args()); - final excludes = config.excludes.map(FileSystem.absolutePath); - final includes = config.includes.map(FileSystem.absolutePath); - final classPaths = - Context.getClassPath().map(FileSystem.absolutePath) - .filter(path -> { - final isRoot = path == FileSystem.absolutePath('.'); - if (Context.defined('watch.excludeRoot') && isRoot) return false; - return !excludes.contains(path); - }); - final paths = dedupePaths(classPaths.concat(includes)); - createServer(port, server -> { - var next: Timer; - var building = false; - var closeRun = cb -> cb(); - function build() { - switch Timer.init(loop) { - case Ok(timer): - if (next != null) { - next.stop(); - } - next = timer; - timer.start(() -> { - if (building) { - build(); - return; + final config = buildArguments(Sys.args()); + final excludes = config.excludes.map(FileSystem.absolutePath); + final includes = config.includes.map(FileSystem.absolutePath); + final classPaths = + Context.getClassPath().map(FileSystem.absolutePath) + .filter(path -> { + final isRoot = path == FileSystem.absolutePath('.'); + if (Context.defined('watch.excludeRoot') && isRoot) return false; + return !excludes.contains(path); + }); + var paths = dedupePaths(classPaths.concat(includes)); + var isDone = false; + paths.iter(p -> childDirs(p, paths, (dirs, done) -> { + isDone = done; + paths.concat(dirs); + })); + + createServer(port, server -> { + var next: Timer = null; + var building = false; + var closeRun = cb -> cb(); + function build() { + switch Timer.init(loop) { + case Ok(timer): + if (next != null) { + next.stop(); + } + next = timer; + timer.start(() -> { + if (building) { + build(); + return; + } + building = true; + final start = Sys.time(); + if (Context.defined('watch.verbose')) + Sys.println('\x1b[32m> Build started\x1b[39m'); + server.build(config, (hasError: Bool) -> { + building = false; + final duration = (Sys.time() - start) * 1000; + closeRun(() -> { + closeRun = cb -> cb(); + timer.close(() -> { + if (Context.defined('watch.verbose')) { + final status = if (hasError) 31 else 32; + Sys.println('\x1b[${status}m> Build finished\x1b[39m'); + } + if (hasError) { + Sys.println('\x1b[90m> Found errors\x1b[39m'); + } else { + Sys.println('\x1b[36m> Build completed in ${formatDuration(duration)}\x1b[39m'); + switch Context.definedValue('watch.run') { + case null: + case v: closeRun = runCommand(v); + } + } + }); + }); + }); + }, 100); + case Error(e): fail('Could not init time', e); + } + } + function watch() { + for (path in paths) { + switch FsEvent.init(loop) { + case Ok(watcher): + watcher.start(path, [], + res -> + switch res { + case Ok({file: (_.toString()) => file}): + if (StringTools.endsWith(file, '.hx')) { + for (exclude in excludes) { + if (isSubOf(FileSystem.absolutePath(file), exclude)) + return; + } + build(); + } + case Error(e): + } + ); + case Error(e): fail('Could not watch $path', e); + } } - building = true; - final start = Sys.time(); - if (Context.defined('watch.verbose')) - Sys.println('\x1b[32m> Build started\x1b[39m'); - server.build(config, (hasError: Bool) -> { - building = false; - final duration = (Sys.time() - start) * 1000; - closeRun(() -> { - closeRun = cb -> cb(); - timer.close(() -> { - if (Context.defined('watch.verbose')) { - final status = if (hasError) 31 else 32; - Sys.println('\x1b[${status}m> Build finished\x1b[39m'); - } - if (hasError) { - Sys.println('\x1b[90m> Found errors\x1b[39m'); - } else { - Sys.println('\x1b[36m> Build completed in ${formatDuration(duration)}\x1b[39m'); - switch Context.definedValue('watch.run') { - case null: - case v: closeRun = runCommand(v); + } + switch Idle.init(loop) { + case Ok(idle): idle.start(() -> { + if (isDone) { + idle.stop(); + build(); + watch(); } - } }); - }); - }); - }, 100); - case Error(e): fail('Could not init time', e); - } - } - function watch() { - for (path in paths) { - switch FsEvent.init(loop) { - case Ok(watcher): - watcher.start(path, [ - FsEventFlag.FS_EVENT_RECURSIVE - ], res -> - switch res { - case Ok({file: (_.toString()) => file}): - if (StringTools.endsWith(file, '.hx')) { - for (exclude in excludes) { - if (isSubOf(FileSystem.absolutePath(file), exclude)) - return; - } - build(); - } - case Error(e): - } - ); - case Error(e): fail('Could not watch $path', e); + case Error(e): fail('Could not get paths', e); } - } - } - build(); - watch(); - }); + }); }); loop.loop(); - } - \ No newline at end of file +} From 3be0e40f9cd18a6cfe08d4981adc3bde4a370b26 Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Wed, 30 Oct 2024 00:44:21 +0100 Subject: [PATCH 03/11] feat: Allow for defining extensions to watch --- readme.md | 15 ++++++++------- src/watch/Watch.hx | 19 ++++++++++++------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/readme.md b/readme.md index 0d396eb..46648ce 100644 --- a/readme.md +++ b/readme.md @@ -23,13 +23,14 @@ All of these are optional. | Define | Description | | --------------------------- | ------------------------------------------------------------------------------------------- | -| `-D watch.run=(string)` | command to execute on successful builds | -| `-D watch.port=(integer)` | use this port for the completion server | -| `-D watch.connect` | connect to a running completion server (use with `watch.port`) | -| `-D watch.excludeRoot` | exclude watching the root directory (see [#3](https://github.com/benmerckx/watch/issues/3)) | -| `-D watch.exclude=(string)` | exclude this path from the watcher (can be repeated for multiple paths) | -| `-D watch.include=(string)` | include this path in the watcher (can be repeated for multiple paths) | -| `-D watch.verbose` | extra log before and after every build | +| `-D watch.run=(string)` | command to execute on successful builds | +| `-D watch.port=(integer)` | use this port for the completion server | +| `-D watch.connect` | connect to a running completion server (use with `watch.port`) | +| `-D watch.excludeRoot` | exclude watching the root directory (see [#3](https://github.com/benmerckx/watch/issues/3)) | +| `-D watch.exclude=(string)` | exclude this path from the watcher (can be repeated for multiple paths) | +| `-D watch.include=(string)` | include this path in the watcher (can be repeated for multiple paths) | +| `-D watch.verbose` | extra log before and after every build | +| `-D watch.extensions=(string)`| extensions to watch, sperated by comma but without dot prefix (will watch '.hx files per default) | ### Example diff --git a/src/watch/Watch.hx b/src/watch/Watch.hx index 9fcc577..8ae43d9 100644 --- a/src/watch/Watch.hx +++ b/src/watch/Watch.hx @@ -1,21 +1,21 @@ package watch; import eval.NativeString; -import eval.luv.Timer; +import eval.luv.Dir; import eval.luv.FsEvent; +import eval.luv.Idle; import eval.luv.Process; +import eval.luv.Result; import eval.luv.SockAddr; import eval.luv.Tcp; -import eval.luv.Result; -import eval.luv.Idle; -import eval.luv.Dir; +import eval.luv.Timer; import haxe.ds.Option; -import haxe.macro.Context; import haxe.io.Path; +import haxe.macro.Context; import sys.FileSystem; -using StringTools; using Lambda; +using StringTools; private final loop = sys.thread.Thread.current().events; @@ -427,7 +427,12 @@ function register() { res -> switch res { case Ok({file: (_.toString()) => file}): - if (StringTools.endsWith(file, '.hx')) { + final extensions = switch Context.definedValue('watch.extensions') { + case null: ['.hx']; + case v: v.split(',').map(s -> '.$s'); + } + final resExtension = file.substring(file.lastIndexOf('.')); + if (extensions.contains(resExtension)) { for (exclude in excludes) { if (isSubOf(FileSystem.absolutePath(file), exclude)) return; From 74ddd81e62abf305d51aa767306de9a70864f613 Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Wed, 30 Oct 2024 00:47:33 +0100 Subject: [PATCH 04/11] fix typo and spacing --- readme.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/readme.md b/readme.md index 46648ce..30088ea 100644 --- a/readme.md +++ b/readme.md @@ -21,16 +21,16 @@ Alternatively, just run it: All of these are optional. -| Define | Description | -| --------------------------- | ------------------------------------------------------------------------------------------- | -| `-D watch.run=(string)` | command to execute on successful builds | -| `-D watch.port=(integer)` | use this port for the completion server | -| `-D watch.connect` | connect to a running completion server (use with `watch.port`) | -| `-D watch.excludeRoot` | exclude watching the root directory (see [#3](https://github.com/benmerckx/watch/issues/3)) | -| `-D watch.exclude=(string)` | exclude this path from the watcher (can be repeated for multiple paths) | -| `-D watch.include=(string)` | include this path in the watcher (can be repeated for multiple paths) | -| `-D watch.verbose` | extra log before and after every build | -| `-D watch.extensions=(string)`| extensions to watch, sperated by comma but without dot prefix (will watch '.hx files per default) | +| Define | Description | +| --------------------------- | ------------------------------------------------------------------------------------------------- | +| `-D watch.run=(string)` | command to execute on successful builds | +| `-D watch.port=(integer)` | use this port for the completion server | +| `-D watch.connect` | connect to a running completion server (use with `watch.port`) | +| `-D watch.excludeRoot` | exclude watching the root directory (see [#3](https://github.com/benmerckx/watch/issues/3)) | +| `-D watch.exclude=(string)` | exclude this path from the watcher (can be repeated for multiple paths) | +| `-D watch.include=(string)` | include this path in the watcher (can be repeated for multiple paths) | +| `-D watch.verbose` | extra log before and after every build | +| `-D watch.extensions=(string)`| extensions to watch, seperated by comma but without dot prefix (will watch '.hx files per default)| ### Example From 3bbbffa22c492cf728540a678431870bedab6c28 Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Thu, 31 Oct 2024 00:10:41 +0100 Subject: [PATCH 05/11] Restore whitespace --- src/watch/Watch.hx | 714 ++++++++++++++++++++++----------------------- 1 file changed, 357 insertions(+), 357 deletions(-) diff --git a/src/watch/Watch.hx b/src/watch/Watch.hx index 9fcc577..9d0b85b 100644 --- a/src/watch/Watch.hx +++ b/src/watch/Watch.hx @@ -1,75 +1,75 @@ package watch; import eval.NativeString; -import eval.luv.Timer; +import eval.luv.Dir; import eval.luv.FsEvent; +import eval.luv.Idle; import eval.luv.Process; +import eval.luv.Result; import eval.luv.SockAddr; import eval.luv.Tcp; -import eval.luv.Result; -import eval.luv.Idle; -import eval.luv.Dir; +import eval.luv.Timer; import haxe.ds.Option; -import haxe.macro.Context; import haxe.io.Path; +import haxe.macro.Context; import sys.FileSystem; -using StringTools; using Lambda; +using StringTools; private final loop = sys.thread.Thread.current().events; private function fail(message: String, ?error: Dynamic) { - Sys.println(message); - if (error != null) - Sys.print('$error'); - Sys.exit(1); + Sys.println(message); + if (error != null) + Sys.print('$error'); + Sys.exit(1); } private final noInputOptions = [ - 'interp', 'haxelib-global', 'no-traces', 'no-output', 'no-inline', 'no-opt', - 'v', 'verbose', 'debug', 'prompt', 'times', 'next', 'each', 'flash-strict' - ]; + 'interp', 'haxelib-global', 'no-traces', 'no-output', 'no-inline', 'no-opt', + 'v', 'verbose', 'debug', 'prompt', 'times', 'next', 'each', 'flash-strict' +]; private final outputs = ['php', 'cpp', 'cs', 'java']; function buildArguments(args: Array): BuildConfig { - final arguments = []; - final excludes = []; - final includes = []; - final forward = []; - final dist = []; - var i = 0; - function skip() i++; - while (i < args.length) { - switch [args[i], args[i + 1]] { - case - ['-L' | '-lib' | '--library', 'watch'], - ['--macro', 'watch.Watch.register()']: - skip(); - case ['-D' | '--define', define] if (define.startsWith('watch.exclude')): - excludes.push(define.substr(define.indexOf('=') + 1)); - skip(); - case ['-D' | '--define', define] if (define.startsWith('watch.include')): - includes.push(define.substr(define.indexOf('=') + 1)); - skip(); - case [arg, next]: - final option = arg.startsWith('--') ? arg.substr(2) : arg.substr(1); - if (outputs.indexOf(option) > -1) - dist.push(next); - forward.push(args[i]); - } + final arguments = []; + final excludes = []; + final includes = []; + final forward = []; + final dist = []; + var i = 0; + function skip() i++; + while (i < args.length) { + switch [args[i], args[i + 1]] { + case + ['-L' | '-lib' | '--library', 'watch'], + ['--macro', 'watch.Watch.register()']: + skip(); + case ['-D' | '--define', define] if (define.startsWith('watch.exclude')): + excludes.push(define.substr(define.indexOf('=') + 1)); + skip(); + case ['-D' | '--define', define] if (define.startsWith('watch.include')): + includes.push(define.substr(define.indexOf('=') + 1)); skip(); + case [arg, next]: + final option = arg.startsWith('--') ? arg.substr(2) : arg.substr(1); + if (outputs.indexOf(option) > -1) + dist.push(next); + forward.push(args[i]); } - var inputExpected = false; - for (arg in forward) { - final isOption = arg.startsWith('-'); + skip(); + } + var inputExpected = false; + for (arg in forward) { + final isOption = arg.startsWith('-'); if (inputExpected && !isOption) arguments[arguments.length - 1] += ' $arg'; else arguments.push(arg); - final option = arg.startsWith('--') ? arg.substr(2) : arg.substr(1); + final option = arg.startsWith('--') ? arg.substr(2) : arg.substr(1); inputExpected = isOption && noInputOptions.indexOf(option) == -1; - } + } return {arguments: arguments, excludes: excludes, includes: includes, dist: dist} } @@ -81,32 +81,32 @@ typedef BuildConfig = { } function isSubOf(path: String, parent: String) { - var a = Path.normalize(path); - var b = Path.normalize(parent); - final caseInsensitive = Sys.systemName() == 'Windows'; - if (caseInsensitive) { - a = a.toLowerCase(); - b = b.toLowerCase(); - } - return a.startsWith(b + '/'); + var a = Path.normalize(path); + var b = Path.normalize(parent); + final caseInsensitive = Sys.systemName() == 'Windows'; + if (caseInsensitive) { + a = a.toLowerCase(); + b = b.toLowerCase(); + } + return a.startsWith(b + '/'); } function pathIsIn(path: String, candidates: Array) { - return candidates.exists(parent -> path == parent || isSubOf(path, parent)); + return candidates.exists(parent -> path == parent || isSubOf(path, parent)); } function dedupePaths(paths: Array) { - final res = []; - final todo = paths.slice(0); - todo.sort((a, b) -> { - return b.length - a.length; - }); + final res = []; + final todo = paths.slice(0); + todo.sort((a, b) -> { + return b.length - a.length; + }); for (i in 0 ...todo.length) { - final path = todo[i]; - final isSubOfNext = pathIsIn(path, todo.slice(i + 1)); - if (!isSubOfNext) res.push(path); - } - return res; + final path = todo[i]; + final isSubOfNext = pathIsIn(path, todo.slice(i + 1)); + if (!isSubOfNext) res.push(path); + } + return res; } typedef Server = { @@ -115,343 +115,343 @@ typedef Server = { } function createServer(port: Int, cb: (server: Server) -> Void) { - if (Context.defined('watch.connect')) - return cb({ - build: (config, done) -> createBuild(port, config, done), - close: (done) -> done() + if (Context.defined('watch.connect')) + return cb({ + build: (config, done) -> createBuild(port, config, done), + close: (done) -> done() + }); + final stdout = Process.inheritFd(Process.stdout, Process.stdout); + final stderr = Process.inheritFd(Process.stderr, Process.stderr); + function start(extension = '') { + switch Process.spawn(loop, 'haxe' + extension, ['haxe', '--wait', '$port'], { + redirect: [stdout, stderr], + onExit: (_, exitStatus, _) -> fail('Completion server exited', exitStatus) + }) { + case Ok(process): + cb({ + build: (config, done) -> { + createBuild(port, config, done); + }, + close: (done) -> process.close(done) }); - final stdout = Process.inheritFd(Process.stdout, Process.stdout); - final stderr = Process.inheritFd(Process.stderr, Process.stderr); - function start(extension = '') { - switch Process.spawn(loop, 'haxe' + extension, ['haxe', '--wait', '$port'], { - redirect: [stdout, stderr], - onExit: (_, exitStatus, _) -> fail('Completion server exited', exitStatus) - }) { - case Ok(process): - cb({ - build: (config, done) -> { - createBuild(port, config, done); - }, - close: (done) -> process.close(done) - }); - case Error(UV_ENOENT) if (Sys.systemName() == 'Windows' && extension == ''): - start('.cmd'); - case Error(e): fail('Could not start completion server, is haxe in path?', e); - } - } - switch [SockAddr.ipv4('127.0.0.1', port), Tcp.init(loop)] { - case [Ok(addr), Ok(socket)]: - socket.connect(addr, res -> switch res { - case Ok(_): - socket.close(() -> { - createServer(port + 1, cb); - }); - case Error(_): - socket.close(() -> start()); - }); - case [_, Error(e)] | [Error(e), _]: - fail('Could not check if port is open', e); + case Error(UV_ENOENT) if (Sys.systemName() == 'Windows' && extension == ''): + start('.cmd'); + case Error(e): fail('Could not start completion server, is haxe in path?', e); } + } + switch [SockAddr.ipv4('127.0.0.1', port), Tcp.init(loop)] { + case [Ok(addr), Ok(socket)]: + socket.connect(addr, res -> switch res { + case Ok(_): + socket.close(() -> { + createServer(port + 1, cb); + }); + case Error(_): + socket.close(() -> start()); + }); + case [_, Error(e)] | [Error(e), _]: + fail('Could not check if port is open', e); + } } private function shellOut(command: String) { - return switch Sys.systemName() { - case 'Windows': ['cmd.exe', '/c', command]; - default: ['sh', '-c', command]; - } -} + return switch Sys.systemName() { + case 'Windows': ['cmd.exe', '/c', command]; + default: ['sh', '-c', command]; + } +} function runCommand(command: String) { - final args = shellOut(command).map(NativeString.fromString); - final stdout = Process.inheritFd(Process.stdout, Process.stdout); - final stderr = Process.inheritFd(Process.stderr, Process.stderr); - var exited = false; - return switch Process.spawn(loop, args[0], args, { - redirect: [stdout, stderr], - onExit: (_, _, _) -> exited = true - }) { - case Ok(process): cb -> { - if (!exited) { - final pid = process.pid(); - final tree = [pid => []]; - final pidsToProcess = [pid => true]; - buildProcessTree(pid, tree, pidsToProcess, parentPid -> { - // Get processes with parent pid - final psargs = '-o pid --no-headers --ppid $parentPid'; - final ps = new sys.io.Process('ps $psargs'); - ps; - }, () -> { - killAll(tree, _ -> cb()); - }); - } - process.close(cb); - } - case Error(e): - Sys.stderr().writeString('Could not run "$command", because $e'); - cb -> cb(); + final args = shellOut(command).map(NativeString.fromString); + final stdout = Process.inheritFd(Process.stdout, Process.stdout); + final stderr = Process.inheritFd(Process.stderr, Process.stderr); + var exited = false; + return switch Process.spawn(loop, args[0], args, { + redirect: [stdout, stderr], + onExit: (_, _, _) -> exited = true + }) { + case Ok(process): cb -> { + if (!exited) { + final pid = process.pid(); + final tree = [pid => []]; + final pidsToProcess = [pid => true]; + buildProcessTree(pid, tree, pidsToProcess, parentPid -> { + // Get processes with parent pid + final psargs = '-o pid --no-headers --ppid $parentPid'; + final ps = new sys.io.Process('ps $psargs'); + ps; + }, () -> { + killAll(tree, _ -> cb()); + }); + } + process.close(cb); } + case Error(e): + Sys.stderr().writeString('Could not run "$command", because $e'); + cb -> cb(); + } } -function buildProcessTree(parentPid:Int, tree:Map>, pidsToProcess:Map, getChildPpid:(pid:Int) -> sys.io.Process, cb:() -> Void) { - final result = getChildPpid(parentPid); - if (result.exitCode() == 0) { - pidsToProcess.remove(parentPid); - final pid = Std.parseInt(result.stdout.readAll().toString()); - final children = tree.get(parentPid) ?? []; - if (!children.has(pid)) { - children.push(pid); - } - tree.set(parentPid, children); - tree.set(pid, []); - pidsToProcess.set(pid, true); - buildProcessTree(pid, tree, pidsToProcess, getChildPpid, cb); - } else { - pidsToProcess.remove(parentPid); - cb(); +function buildProcessTree(parentPid: Int, tree: Map>, pidsToProcess: Map, getChildPpid: (pid: Int) -> sys.io.Process, cb:() -> Void) { + final result = getChildPpid(parentPid); + if (result.exitCode() == 0) { + pidsToProcess.remove(parentPid); + final pid = Std.parseInt(result.stdout.readAll().toString()); + final children = tree.get(parentPid) ?? []; + if (!children.has(pid)) { + children.push(pid); } + tree.set(parentPid, children); + tree.set(pid, []); + pidsToProcess.set(pid, true); + buildProcessTree(pid, tree, pidsToProcess, getChildPpid, cb); + } else { + pidsToProcess.remove(parentPid); + cb(); + } - result.close(); + result.close(); } -function killAll(tree:Map>, callback:(error:Option) -> Void) { - final killed:Map = []; - try { - [for (k in tree.keys()) k].iter(pid -> { - tree.get(pid).iter(pidpid -> { - final isKilled = killed.get(pidpid) ?? false; - if (!isKilled) { - switch Process.killPid(pidpid, SIGKILL) { - case Ok(_): - killed.set(pidpid, true); - case Error(e): - } - } - }); - if (!(killed.get(pid) ?? false)) { - switch Process.killPid(pid, SIGKILL) { - case Ok(_): - killed.set(pid, true); - case Error(e): - } - } - }); - if (callback != null) { - return callback(None); +function killAll(tree: Map>, callback: (error: Option) -> Void) { + final killed: Map = []; + try { + [for (k in tree.keys()) k].iter(pid -> { + tree.get(pid).iter(pidpid -> { + final isKilled = killed.get(pidpid) ?? false; + if (!isKilled) { + switch Process.killPid(pidpid, SIGKILL) { + case Ok(_): + killed.set(pidpid, true); + case Error(e): + } } - } catch (err) { - if (callback != null) { - return callback(Some(err.toString())); - } else { - throw err; + }); + if (!(killed.get(pid) ?? false)) { + switch Process.killPid(pid, SIGKILL) { + case Ok(_): + killed.set(pid, true); + case Error(e): } + } + }); + if (callback != null) { + return callback(None); } + } catch (err) { + if (callback != null) { + return callback(Some(err.toString())); + } else { + throw err; + } + } } function createBuild(port: Int, config: BuildConfig, done: (hasError: Bool) -> Void, retry = 0) { - if (retry > 1000) fail('Could not connect to port $port'); - switch [ - SockAddr.ipv4('127.0.0.1', port), - Tcp.init(loop) - ] { - case [Ok(addr), Ok(socket)]: - socket.connect(addr, res -> - switch res { - case Ok(_): - var hasError = false; - socket.readStart(res -> switch res { - case Ok(_.toString() => data): - for (line in data.split('\n')) { - switch (line.charCodeAt(0)) { - case 0x01: - Sys.print(line.substr(1).split('\x01').join('\n')); - case 0x02: - hasError = true; - default: - if (line.length > 0) { - Sys.stderr().writeString(line + '\n'); - Sys.stderr().flush(); - } - } - } - case Error(UV_EOF): - socket.close(() -> done(hasError)); - case Error(e): - fail('Server closed', e); - }); - socket.write([config.arguments.join('\n') + '\000'], (res, bytesWritten) -> switch res { - case Ok(_): - case Error(e): fail('Could not write to server', e); - }); - case Error(UV_ECONNREFUSED): - socket.close(() -> createBuild(port, config, done, retry + 1)); - case Error(e): - fail('Could not connect to server', e); - } - ); - case [_, Error(e)] | [Error(e), _]: + if (retry > 1000) fail('Could not connect to port $port'); + switch [ + SockAddr.ipv4('127.0.0.1', port), + Tcp.init(loop) + ] { + case [Ok(addr), Ok(socket)]: + socket.connect(addr, res -> + switch res { + case Ok(_): + var hasError = false; + socket.readStart(res -> switch res { + case Ok(_.toString() => data): + for (line in data.split('\n')) { + switch (line.charCodeAt(0)) { + case 0x01: + Sys.print(line.substr(1).split('\x01').join('\n')); + case 0x02: + hasError = true; + default: + if (line.length > 0) { + Sys.stderr().writeString(line + '\n'); + Sys.stderr().flush(); + } + } + } + case Error(UV_EOF): + socket.close(() -> done(hasError)); + case Error(e): + fail('Server closed', e); + }); + socket.write([config.arguments.join('\n') + '\000'], (res, bytesWritten) -> switch res { + case Ok(_): + case Error(e): fail('Could not write to server', e); + }); + case Error(UV_ECONNREFUSED): + socket.close(() -> createBuild(port, config, done, retry + 1)); + case Error(e): fail('Could not connect to server', e); - } + } + ); + case [_, Error(e)] | [Error(e), _]: + fail('Could not connect to server', e); + } } function formatDuration(duration: Float) { - if (duration < 1000) - return '${Math.round(duration)}ms'; - final precision = 100; - final s = Math.round(duration / 1000 * precision) / precision; - return '${s}s'; + if (duration < 1000) + return '${Math.round(duration)}ms'; + final precision = 100; + final s = Math.round(duration / 1000 * precision) / precision; + return '${s}s'; } function getFreePort(done: (port: haxe.ds.Option) -> Void) { - return switch [ - SockAddr.ipv4('127.0.0.1', 0), - Tcp.init(loop) - ] { - case [Ok(addr), Ok(socket)]: - switch socket.bind(addr) { - case Ok(_): - switch socket.getSockName() { - case Ok(addr): - socket.close(() -> done(Some(addr.port))); - default: - socket.close(() -> done(None)); - } - default: done(None); - } + return switch [ + SockAddr.ipv4('127.0.0.1', 0), + Tcp.init(loop) + ] { + case [Ok(addr), Ok(socket)]: + switch socket.bind(addr) { + case Ok(_): + switch socket.getSockName() { + case Ok(addr): + socket.close(() -> done(Some(addr.port))); + default: + socket.close(() -> done(None)); + } default: done(None); - } + } + default: done(None); + } } -function childDirs(path:String, dirs:Array, cb:(dirs:Array, done:Bool) -> Void) { - final numDirs = dirs.length; - Dir.scan(loop, path, result -> { - switch result { - case Ok(dirScan): - var dirent:Dirent = dirScan.next(); - while (dirent != null) { - if (dirent.kind == DirentKind.DIR) { - final d = '${path}/${dirent.name.toString()}'; - dirs.push(d); - childDirs(d, dirs, cb); - } - dirent = dirScan.next(); - } - cb(dirs, dirs.length == numDirs); - case Error(e): - fail('Could not read child dir of $path', e); +function childDirs(path: String, dirs: Array, cb: (dirs: Array, done: Bool) -> Void) { + final numDirs = dirs.length; + Dir.scan(loop, path, result -> { + switch result { + case Ok(dirScan): + var dirent:Dirent = dirScan.next(); + while (dirent != null) { + if (dirent.kind == DirentKind.DIR) { + final d = '${path}/${dirent.name.toString()}'; + dirs.push(d); + childDirs(d, dirs, cb); + } + dirent = dirScan.next(); } - }); + cb(dirs, dirs.length == numDirs); + case Error(e): + fail('Could not read child dir of $path', e); + } + }); } function register() { - function getPort(done: (port: Int) -> Void) { - switch Context.definedValue('watch.port') { - case null: - getFreePort(res -> - switch res { - case Some(port): done(port); - default: fail('Could not find free port'); - } - ); - case v: done(Std.parseInt(v)); - } + function getPort(done: (port: Int) -> Void) { + switch Context.definedValue('watch.port') { + case null: + getFreePort(res -> + switch res { + case Some(port): done(port); + default: fail('Could not find free port'); + } + ); + case v: done(Std.parseInt(v)); } - getPort(port -> { - final config = buildArguments(Sys.args()); - final excludes = config.excludes.map(FileSystem.absolutePath); - final includes = config.includes.map(FileSystem.absolutePath); - final classPaths = - Context.getClassPath().map(FileSystem.absolutePath) - .filter(path -> { - final isRoot = path == FileSystem.absolutePath('.'); - if (Context.defined('watch.excludeRoot') && isRoot) return false; - return !excludes.contains(path); - }); + } + getPort(port -> { + final config = buildArguments(Sys.args()); + final excludes = config.excludes.map(FileSystem.absolutePath); + final includes = config.includes.map(FileSystem.absolutePath); + final classPaths = + Context.getClassPath().map(FileSystem.absolutePath) + .filter(path -> { + final isRoot = path == FileSystem.absolutePath('.'); + if (Context.defined('watch.excludeRoot') && isRoot) return false; + return !excludes.contains(path); + }); var paths = dedupePaths(classPaths.concat(includes)); var isDone = false; paths.iter(p -> childDirs(p, paths, (dirs, done) -> { - isDone = done; - paths.concat(dirs); + isDone = done; + paths.concat(dirs); })); - - createServer(port, server -> { - var next: Timer = null; - var building = false; - var closeRun = cb -> cb(); - function build() { - switch Timer.init(loop) { - case Ok(timer): - if (next != null) { - next.stop(); - } - next = timer; - timer.start(() -> { - if (building) { - build(); - return; - } - building = true; - final start = Sys.time(); - if (Context.defined('watch.verbose')) - Sys.println('\x1b[32m> Build started\x1b[39m'); - server.build(config, (hasError: Bool) -> { - building = false; - final duration = (Sys.time() - start) * 1000; - closeRun(() -> { - closeRun = cb -> cb(); - timer.close(() -> { - if (Context.defined('watch.verbose')) { - final status = if (hasError) 31 else 32; - Sys.println('\x1b[${status}m> Build finished\x1b[39m'); - } - if (hasError) { - Sys.println('\x1b[90m> Found errors\x1b[39m'); - } else { - Sys.println('\x1b[36m> Build completed in ${formatDuration(duration)}\x1b[39m'); - switch Context.definedValue('watch.run') { - case null: - case v: closeRun = runCommand(v); - } - } - }); - }); - }); - }, 100); - case Error(e): fail('Could not init time', e); - } + createServer(port, server -> { + var next: Timer; + var building = false; + var closeRun = cb -> cb(); + function build() { + switch Timer.init(loop) { + case Ok(timer): + if (next != null) { + next.stop(); } - function watch() { - for (path in paths) { - switch FsEvent.init(loop) { - case Ok(watcher): - watcher.start(path, [], - res -> - switch res { - case Ok({file: (_.toString()) => file}): - if (StringTools.endsWith(file, '.hx')) { - for (exclude in excludes) { - if (isSubOf(FileSystem.absolutePath(file), exclude)) - return; - } - build(); - } - case Error(e): - } - ); - case Error(e): fail('Could not watch $path', e); + next = timer; + timer.start(() -> { + if (building) { + build(); + return; + } + building = true; + final start = Sys.time(); + if (Context.defined('watch.verbose')) + Sys.println('\x1b[32m> Build started\x1b[39m'); + server.build(config, (hasError: Bool) -> { + building = false; + final duration = (Sys.time() - start) * 1000; + closeRun(() -> { + closeRun = cb -> cb(); + timer.close(() -> { + if (Context.defined('watch.verbose')) { + final status = if (hasError) 31 else 32; + Sys.println('\x1b[${status}m> Build finished\x1b[39m'); } + if (hasError) { + Sys.println('\x1b[90m> Found errors\x1b[39m'); + } else { + Sys.println('\x1b[36m> Build completed in ${formatDuration(duration)}\x1b[39m'); + switch Context.definedValue('watch.run') { + case null: + case v: closeRun = runCommand(v); + } + } + }); + }); + }); + }, 100); + case Error(e): fail('Could not init time', e); + } + } + function watch() { + for (path in paths) { + switch FsEvent.init(loop) { + case Ok(watcher): + watcher.start(path, [], + res -> + switch res { + case Ok({file: (_.toString()) => file}): + if (StringTools.endsWith(file, '.hx')) { + for (exclude in excludes) { + if (isSubOf(FileSystem.absolutePath(file), exclude)) + return; + } + build(); + } + case Error(e): } + ); + case Error(e): fail('Could not watch $path', e); + } + } + } + switch Idle.init(loop) { + case Ok(idle): + idle.start(() -> { + if (isDone) { + idle.stop(); + build(); + watch(); } - switch Idle.init(loop) { - case Ok(idle): idle.start(() -> { - if (isDone) { - idle.stop(); - build(); - watch(); - } - }); - case Error(e): fail('Could not get paths', e); - } - }); + }); + case Error(e): fail('Could not get paths', e); + } }); - loop.loop(); + }); + loop.loop(); } From 0c0f0182c0080a5bb9c1ef9cba187a8865ce82bd Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Thu, 31 Oct 2024 00:14:50 +0100 Subject: [PATCH 06/11] Separate out changes for closing process --- src/watch/Watch.hx | 108 +++++---------------------------------------- 1 file changed, 10 insertions(+), 98 deletions(-) diff --git a/src/watch/Watch.hx b/src/watch/Watch.hx index 9d0b85b..5d92028 100644 --- a/src/watch/Watch.hx +++ b/src/watch/Watch.hx @@ -1,21 +1,16 @@ package watch; import eval.NativeString; -import eval.luv.Dir; +import eval.luv.Timer; import eval.luv.FsEvent; -import eval.luv.Idle; import eval.luv.Process; -import eval.luv.Result; import eval.luv.SockAddr; import eval.luv.Tcp; -import eval.luv.Timer; -import haxe.ds.Option; -import haxe.io.Path; import haxe.macro.Context; +import haxe.io.Path; import sys.FileSystem; - -using Lambda; using StringTools; +using Lambda; private final loop = sys.thread.Thread.current().events; @@ -171,19 +166,12 @@ function runCommand(command: String) { onExit: (_, _, _) -> exited = true }) { case Ok(process): cb -> { - if (!exited) { - final pid = process.pid(); - final tree = [pid => []]; - final pidsToProcess = [pid => true]; - buildProcessTree(pid, tree, pidsToProcess, parentPid -> { - // Get processes with parent pid - final psargs = '-o pid --no-headers --ppid $parentPid'; - final ps = new sys.io.Process('ps $psargs'); - ps; - }, () -> { - killAll(tree, _ -> cb()); - }); - } + // process.kill results in "Uncaught exception Cannot call null" + if (!exited) + switch Process.killPid(process.pid(), SIGKILL) { + case Ok(_): + case Error(e): fail('Could not end run command', e); + } process.close(cb); } case Error(e): @@ -192,61 +180,6 @@ function runCommand(command: String) { } } -function buildProcessTree(parentPid: Int, tree: Map>, pidsToProcess: Map, getChildPpid: (pid: Int) -> sys.io.Process, cb:() -> Void) { - final result = getChildPpid(parentPid); - if (result.exitCode() == 0) { - pidsToProcess.remove(parentPid); - final pid = Std.parseInt(result.stdout.readAll().toString()); - final children = tree.get(parentPid) ?? []; - if (!children.has(pid)) { - children.push(pid); - } - tree.set(parentPid, children); - tree.set(pid, []); - pidsToProcess.set(pid, true); - buildProcessTree(pid, tree, pidsToProcess, getChildPpid, cb); - } else { - pidsToProcess.remove(parentPid); - cb(); - } - - result.close(); -} - -function killAll(tree: Map>, callback: (error: Option) -> Void) { - final killed: Map = []; - try { - [for (k in tree.keys()) k].iter(pid -> { - tree.get(pid).iter(pidpid -> { - final isKilled = killed.get(pidpid) ?? false; - if (!isKilled) { - switch Process.killPid(pidpid, SIGKILL) { - case Ok(_): - killed.set(pidpid, true); - case Error(e): - } - } - }); - if (!(killed.get(pid) ?? false)) { - switch Process.killPid(pid, SIGKILL) { - case Ok(_): - killed.set(pid, true); - case Error(e): - } - } - }); - if (callback != null) { - return callback(None); - } - } catch (err) { - if (callback != null) { - return callback(Some(err.toString())); - } else { - throw err; - } - } -} - function createBuild(port: Int, config: BuildConfig, done: (hasError: Bool) -> Void, retry = 0) { if (retry > 1000) fail('Could not connect to port $port'); switch [ @@ -321,27 +254,6 @@ function getFreePort(done: (port: haxe.ds.Option) -> Void) { } } -function childDirs(path: String, dirs: Array, cb: (dirs: Array, done: Bool) -> Void) { - final numDirs = dirs.length; - Dir.scan(loop, path, result -> { - switch result { - case Ok(dirScan): - var dirent:Dirent = dirScan.next(); - while (dirent != null) { - if (dirent.kind == DirentKind.DIR) { - final d = '${path}/${dirent.name.toString()}'; - dirs.push(d); - childDirs(d, dirs, cb); - } - dirent = dirScan.next(); - } - cb(dirs, dirs.length == numDirs); - case Error(e): - fail('Could not read child dir of $path', e); - } - }); -} - function register() { function getPort(done: (port: Int) -> Void) { switch Context.definedValue('watch.port') { @@ -454,4 +366,4 @@ function register() { }); }); loop.loop(); -} +} \ No newline at end of file From e7a5906da183c0451e8cff8220ce5910eb72975d Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Thu, 31 Oct 2024 00:18:04 +0100 Subject: [PATCH 07/11] Isolate changes for changing extension --- src/watch/Watch.hx | 659 +++++++++++++++++++-------------------------- 1 file changed, 279 insertions(+), 380 deletions(-) diff --git a/src/watch/Watch.hx b/src/watch/Watch.hx index 8ae43d9..f240b43 100644 --- a/src/watch/Watch.hx +++ b/src/watch/Watch.hx @@ -1,75 +1,70 @@ package watch; import eval.NativeString; -import eval.luv.Dir; +import eval.luv.Timer; import eval.luv.FsEvent; -import eval.luv.Idle; import eval.luv.Process; -import eval.luv.Result; import eval.luv.SockAddr; import eval.luv.Tcp; -import eval.luv.Timer; -import haxe.ds.Option; -import haxe.io.Path; import haxe.macro.Context; +import haxe.io.Path; import sys.FileSystem; - -using Lambda; using StringTools; +using Lambda; private final loop = sys.thread.Thread.current().events; private function fail(message: String, ?error: Dynamic) { - Sys.println(message); - if (error != null) - Sys.print('$error'); - Sys.exit(1); + Sys.println(message); + if (error != null) + Sys.print('$error'); + Sys.exit(1); } private final noInputOptions = [ - 'interp', 'haxelib-global', 'no-traces', 'no-output', 'no-inline', 'no-opt', - 'v', 'verbose', 'debug', 'prompt', 'times', 'next', 'each', 'flash-strict' - ]; + 'interp', 'haxelib-global', 'no-traces', 'no-output', 'no-inline', 'no-opt', + 'v', 'verbose', 'debug', 'prompt', 'times', 'next', 'each', 'flash-strict' +]; private final outputs = ['php', 'cpp', 'cs', 'java']; function buildArguments(args: Array): BuildConfig { - final arguments = []; - final excludes = []; - final includes = []; - final forward = []; - final dist = []; - var i = 0; - function skip() i++; - while (i < args.length) { - switch [args[i], args[i + 1]] { - case - ['-L' | '-lib' | '--library', 'watch'], - ['--macro', 'watch.Watch.register()']: - skip(); - case ['-D' | '--define', define] if (define.startsWith('watch.exclude')): - excludes.push(define.substr(define.indexOf('=') + 1)); - skip(); - case ['-D' | '--define', define] if (define.startsWith('watch.include')): - includes.push(define.substr(define.indexOf('=') + 1)); - skip(); - case [arg, next]: - final option = arg.startsWith('--') ? arg.substr(2) : arg.substr(1); - if (outputs.indexOf(option) > -1) - dist.push(next); - forward.push(args[i]); - } + final arguments = []; + final excludes = []; + final includes = []; + final forward = []; + final dist = []; + var i = 0; + function skip() i++; + while (i < args.length) { + switch [args[i], args[i + 1]] { + case + ['-L' | '-lib' | '--library', 'watch'], + ['--macro', 'watch.Watch.register()']: + skip(); + case ['-D' | '--define', define] if (define.startsWith('watch.exclude')): + excludes.push(define.substr(define.indexOf('=') + 1)); + skip(); + case ['-D' | '--define', define] if (define.startsWith('watch.include')): + includes.push(define.substr(define.indexOf('=') + 1)); skip(); + case [arg, next]: + final option = arg.startsWith('--') ? arg.substr(2) : arg.substr(1); + if (outputs.indexOf(option) > -1) + dist.push(next); + forward.push(args[i]); } - var inputExpected = false; - for (arg in forward) { - final isOption = arg.startsWith('-'); + skip(); + } + var inputExpected = false; + for (arg in forward) { + final isOption = arg.startsWith('-'); if (inputExpected && !isOption) arguments[arguments.length - 1] += ' $arg'; else arguments.push(arg); - final option = arg.startsWith('--') ? arg.substr(2) : arg.substr(1); + final option = arg.startsWith('--') ? arg.substr(2) : arg.substr(1); inputExpected = isOption && noInputOptions.indexOf(option) == -1; - } + } return {arguments: arguments, excludes: excludes, includes: includes, dist: dist} } @@ -81,32 +76,32 @@ typedef BuildConfig = { } function isSubOf(path: String, parent: String) { - var a = Path.normalize(path); - var b = Path.normalize(parent); - final caseInsensitive = Sys.systemName() == 'Windows'; - if (caseInsensitive) { - a = a.toLowerCase(); - b = b.toLowerCase(); - } - return a.startsWith(b + '/'); + var a = Path.normalize(path); + var b = Path.normalize(parent); + final caseInsensitive = Sys.systemName() == 'Windows'; + if (caseInsensitive) { + a = a.toLowerCase(); + b = b.toLowerCase(); + } + return a.startsWith(b + '/'); } function pathIsIn(path: String, candidates: Array) { - return candidates.exists(parent -> path == parent || isSubOf(path, parent)); + return candidates.exists(parent -> path == parent || isSubOf(path, parent)); } function dedupePaths(paths: Array) { - final res = []; - final todo = paths.slice(0); - todo.sort((a, b) -> { - return b.length - a.length; - }); + final res = []; + final todo = paths.slice(0); + todo.sort((a, b) -> { + return b.length - a.length; + }); for (i in 0 ...todo.length) { - final path = todo[i]; - final isSubOfNext = pathIsIn(path, todo.slice(i + 1)); - if (!isSubOfNext) res.push(path); - } - return res; + final path = todo[i]; + final isSubOfNext = pathIsIn(path, todo.slice(i + 1)); + if (!isSubOfNext) res.push(path); + } + return res; } typedef Server = { @@ -115,348 +110,252 @@ typedef Server = { } function createServer(port: Int, cb: (server: Server) -> Void) { - if (Context.defined('watch.connect')) - return cb({ - build: (config, done) -> createBuild(port, config, done), - close: (done) -> done() + if (Context.defined('watch.connect')) + return cb({ + build: (config, done) -> createBuild(port, config, done), + close: (done) -> done() + }); + final stdout = Process.inheritFd(Process.stdout, Process.stdout); + final stderr = Process.inheritFd(Process.stderr, Process.stderr); + function start(extension = '') { + switch Process.spawn(loop, 'haxe' + extension, ['haxe', '--wait', '$port'], { + redirect: [stdout, stderr], + onExit: (_, exitStatus, _) -> fail('Completion server exited', exitStatus) + }) { + case Ok(process): + cb({ + build: (config, done) -> { + createBuild(port, config, done); + }, + close: (done) -> process.close(done) }); - final stdout = Process.inheritFd(Process.stdout, Process.stdout); - final stderr = Process.inheritFd(Process.stderr, Process.stderr); - function start(extension = '') { - switch Process.spawn(loop, 'haxe' + extension, ['haxe', '--wait', '$port'], { - redirect: [stdout, stderr], - onExit: (_, exitStatus, _) -> fail('Completion server exited', exitStatus) - }) { - case Ok(process): - cb({ - build: (config, done) -> { - createBuild(port, config, done); - }, - close: (done) -> process.close(done) - }); - case Error(UV_ENOENT) if (Sys.systemName() == 'Windows' && extension == ''): - start('.cmd'); - case Error(e): fail('Could not start completion server, is haxe in path?', e); - } - } - switch [SockAddr.ipv4('127.0.0.1', port), Tcp.init(loop)] { - case [Ok(addr), Ok(socket)]: - socket.connect(addr, res -> switch res { - case Ok(_): - socket.close(() -> { - createServer(port + 1, cb); - }); - case Error(_): - socket.close(() -> start()); - }); - case [_, Error(e)] | [Error(e), _]: - fail('Could not check if port is open', e); + case Error(UV_ENOENT) if (Sys.systemName() == 'Windows' && extension == ''): + start('.cmd'); + case Error(e): fail('Could not start completion server, is haxe in path?', e); } + } + switch [SockAddr.ipv4('127.0.0.1', port), Tcp.init(loop)] { + case [Ok(addr), Ok(socket)]: + socket.connect(addr, res -> switch res { + case Ok(_): + socket.close(() -> { + createServer(port + 1, cb); + }); + case Error(_): + socket.close(() -> start()); + }); + case [_, Error(e)] | [Error(e), _]: + fail('Could not check if port is open', e); + } } private function shellOut(command: String) { - return switch Sys.systemName() { - case 'Windows': ['cmd.exe', '/c', command]; - default: ['sh', '-c', command]; - } -} + return switch Sys.systemName() { + case 'Windows': ['cmd.exe', '/c', command]; + default: ['sh', '-c', command]; + } +} function runCommand(command: String) { - final args = shellOut(command).map(NativeString.fromString); - final stdout = Process.inheritFd(Process.stdout, Process.stdout); - final stderr = Process.inheritFd(Process.stderr, Process.stderr); - var exited = false; - return switch Process.spawn(loop, args[0], args, { - redirect: [stdout, stderr], - onExit: (_, _, _) -> exited = true - }) { - case Ok(process): cb -> { - if (!exited) { - final pid = process.pid(); - final tree = [pid => []]; - final pidsToProcess = [pid => true]; - buildProcessTree(pid, tree, pidsToProcess, parentPid -> { - // Get processes with parent pid - final psargs = '-o pid --no-headers --ppid $parentPid'; - final ps = new sys.io.Process('ps $psargs'); - ps; - }, () -> { - killAll(tree, _ -> cb()); - }); - } - process.close(cb); - } - case Error(e): - Sys.stderr().writeString('Could not run "$command", because $e'); - cb -> cb(); - } -} - -function buildProcessTree(parentPid:Int, tree:Map>, pidsToProcess:Map, getChildPpid:(pid:Int) -> sys.io.Process, cb:() -> Void) { - final result = getChildPpid(parentPid); - if (result.exitCode() == 0) { - pidsToProcess.remove(parentPid); - final pid = Std.parseInt(result.stdout.readAll().toString()); - final children = tree.get(parentPid) ?? []; - if (!children.has(pid)) { - children.push(pid); + final args = shellOut(command).map(NativeString.fromString); + final stdout = Process.inheritFd(Process.stdout, Process.stdout); + final stderr = Process.inheritFd(Process.stderr, Process.stderr); + var exited = false; + return switch Process.spawn(loop, args[0], args, { + redirect: [stdout, stderr], + onExit: (_, _, _) -> exited = true + }) { + case Ok(process): cb -> { + // process.kill results in "Uncaught exception Cannot call null" + if (!exited) + switch Process.killPid(process.pid(), SIGKILL) { + case Ok(_): + case Error(e): fail('Could not end run command', e); } - tree.set(parentPid, children); - tree.set(pid, []); - pidsToProcess.set(pid, true); - buildProcessTree(pid, tree, pidsToProcess, getChildPpid, cb); - } else { - pidsToProcess.remove(parentPid); - cb(); + process.close(cb); } - - result.close(); + case Error(e): + Sys.stderr().writeString('Could not run "$command", because $e'); + cb -> cb(); + } } -function killAll(tree:Map>, callback:(error:Option) -> Void) { - final killed:Map = []; - try { - [for (k in tree.keys()) k].iter(pid -> { - tree.get(pid).iter(pidpid -> { - final isKilled = killed.get(pidpid) ?? false; - if (!isKilled) { - switch Process.killPid(pidpid, SIGKILL) { - case Ok(_): - killed.set(pidpid, true); - case Error(e): - } +function createBuild(port: Int, config: BuildConfig, done: (hasError: Bool) -> Void, retry = 0) { + if (retry > 1000) fail('Could not connect to port $port'); + switch [ + SockAddr.ipv4('127.0.0.1', port), + Tcp.init(loop) + ] { + case [Ok(addr), Ok(socket)]: + socket.connect(addr, res -> + switch res { + case Ok(_): + var hasError = false; + socket.readStart(res -> switch res { + case Ok(_.toString() => data): + for (line in data.split('\n')) { + switch (line.charCodeAt(0)) { + case 0x01: + Sys.print(line.substr(1).split('\x01').join('\n')); + case 0x02: + hasError = true; + default: + if (line.length > 0) { + Sys.stderr().writeString(line + '\n'); + Sys.stderr().flush(); + } + } } + case Error(UV_EOF): + socket.close(() -> done(hasError)); + case Error(e): + fail('Server closed', e); }); - if (!(killed.get(pid) ?? false)) { - switch Process.killPid(pid, SIGKILL) { - case Ok(_): - killed.set(pid, true); - case Error(e): - } - } - }); - if (callback != null) { - return callback(None); - } - } catch (err) { - if (callback != null) { - return callback(Some(err.toString())); - } else { - throw err; - } - } -} - -function createBuild(port: Int, config: BuildConfig, done: (hasError: Bool) -> Void, retry = 0) { - if (retry > 1000) fail('Could not connect to port $port'); - switch [ - SockAddr.ipv4('127.0.0.1', port), - Tcp.init(loop) - ] { - case [Ok(addr), Ok(socket)]: - socket.connect(addr, res -> - switch res { - case Ok(_): - var hasError = false; - socket.readStart(res -> switch res { - case Ok(_.toString() => data): - for (line in data.split('\n')) { - switch (line.charCodeAt(0)) { - case 0x01: - Sys.print(line.substr(1).split('\x01').join('\n')); - case 0x02: - hasError = true; - default: - if (line.length > 0) { - Sys.stderr().writeString(line + '\n'); - Sys.stderr().flush(); - } - } - } - case Error(UV_EOF): - socket.close(() -> done(hasError)); - case Error(e): - fail('Server closed', e); - }); - socket.write([config.arguments.join('\n') + '\000'], (res, bytesWritten) -> switch res { - case Ok(_): - case Error(e): fail('Could not write to server', e); - }); - case Error(UV_ECONNREFUSED): - socket.close(() -> createBuild(port, config, done, retry + 1)); - case Error(e): - fail('Could not connect to server', e); - } - ); - case [_, Error(e)] | [Error(e), _]: + socket.write([config.arguments.join('\n') + '\000'], (res, bytesWritten) -> switch res { + case Ok(_): + case Error(e): fail('Could not write to server', e); + }); + case Error(UV_ECONNREFUSED): + socket.close(() -> createBuild(port, config, done, retry + 1)); + case Error(e): fail('Could not connect to server', e); - } + } + ); + case [_, Error(e)] | [Error(e), _]: + fail('Could not connect to server', e); + } } function formatDuration(duration: Float) { - if (duration < 1000) - return '${Math.round(duration)}ms'; - final precision = 100; - final s = Math.round(duration / 1000 * precision) / precision; - return '${s}s'; + if (duration < 1000) + return '${Math.round(duration)}ms'; + final precision = 100; + final s = Math.round(duration / 1000 * precision) / precision; + return '${s}s'; } function getFreePort(done: (port: haxe.ds.Option) -> Void) { - return switch [ - SockAddr.ipv4('127.0.0.1', 0), - Tcp.init(loop) - ] { - case [Ok(addr), Ok(socket)]: - switch socket.bind(addr) { - case Ok(_): - switch socket.getSockName() { - case Ok(addr): - socket.close(() -> done(Some(addr.port))); - default: - socket.close(() -> done(None)); - } - default: done(None); - } + return switch [ + SockAddr.ipv4('127.0.0.1', 0), + Tcp.init(loop) + ] { + case [Ok(addr), Ok(socket)]: + switch socket.bind(addr) { + case Ok(_): + switch socket.getSockName() { + case Ok(addr): + socket.close(() -> done(Some(addr.port))); + default: + socket.close(() -> done(None)); + } default: done(None); - } -} - -function childDirs(path:String, dirs:Array, cb:(dirs:Array, done:Bool) -> Void) { - final numDirs = dirs.length; - Dir.scan(loop, path, result -> { - switch result { - case Ok(dirScan): - var dirent:Dirent = dirScan.next(); - while (dirent != null) { - if (dirent.kind == DirentKind.DIR) { - final d = '${path}/${dirent.name.toString()}'; - dirs.push(d); - childDirs(d, dirs, cb); - } - dirent = dirScan.next(); - } - cb(dirs, dirs.length == numDirs); - case Error(e): - fail('Could not read child dir of $path', e); - } - }); + } + default: done(None); + } } function register() { - function getPort(done: (port: Int) -> Void) { - switch Context.definedValue('watch.port') { - case null: - getFreePort(res -> - switch res { - case Some(port): done(port); - default: fail('Could not find free port'); - } - ); - case v: done(Std.parseInt(v)); - } + function getPort(done: (port: Int) -> Void) { + switch Context.definedValue('watch.port') { + case null: + getFreePort(res -> + switch res { + case Some(port): done(port); + default: fail('Could not find free port'); + } + ); + case v: done(Std.parseInt(v)); } - getPort(port -> { - final config = buildArguments(Sys.args()); - final excludes = config.excludes.map(FileSystem.absolutePath); - final includes = config.includes.map(FileSystem.absolutePath); - final classPaths = - Context.getClassPath().map(FileSystem.absolutePath) - .filter(path -> { - final isRoot = path == FileSystem.absolutePath('.'); - if (Context.defined('watch.excludeRoot') && isRoot) return false; - return !excludes.contains(path); - }); - var paths = dedupePaths(classPaths.concat(includes)); - var isDone = false; - paths.iter(p -> childDirs(p, paths, (dirs, done) -> { - isDone = done; - paths.concat(dirs); - })); - - createServer(port, server -> { - var next: Timer = null; - var building = false; - var closeRun = cb -> cb(); - function build() { - switch Timer.init(loop) { - case Ok(timer): - if (next != null) { - next.stop(); - } - next = timer; - timer.start(() -> { - if (building) { - build(); - return; - } - building = true; - final start = Sys.time(); - if (Context.defined('watch.verbose')) - Sys.println('\x1b[32m> Build started\x1b[39m'); - server.build(config, (hasError: Bool) -> { - building = false; - final duration = (Sys.time() - start) * 1000; - closeRun(() -> { - closeRun = cb -> cb(); - timer.close(() -> { - if (Context.defined('watch.verbose')) { - final status = if (hasError) 31 else 32; - Sys.println('\x1b[${status}m> Build finished\x1b[39m'); - } - if (hasError) { - Sys.println('\x1b[90m> Found errors\x1b[39m'); - } else { - Sys.println('\x1b[36m> Build completed in ${formatDuration(duration)}\x1b[39m'); - switch Context.definedValue('watch.run') { - case null: - case v: closeRun = runCommand(v); - } - } - }); - }); - }); - }, 100); - case Error(e): fail('Could not init time', e); - } + } + getPort(port -> { + final config = buildArguments(Sys.args()); + final excludes = config.excludes.map(FileSystem.absolutePath); + final includes = config.includes.map(FileSystem.absolutePath); + final classPaths = + Context.getClassPath().map(FileSystem.absolutePath) + .filter(path -> { + final isRoot = path == FileSystem.absolutePath('.'); + if (Context.defined('watch.excludeRoot') && isRoot) return false; + return !excludes.contains(path); + }); + final paths = dedupePaths(classPaths.concat(includes)); + createServer(port, server -> { + var next: Timer; + var building = false; + var closeRun = cb -> cb(); + function build() { + switch Timer.init(loop) { + case Ok(timer): + if (next != null) { + next.stop(); } - function watch() { - for (path in paths) { - switch FsEvent.init(loop) { - case Ok(watcher): - watcher.start(path, [], - res -> - switch res { - case Ok({file: (_.toString()) => file}): - final extensions = switch Context.definedValue('watch.extensions') { - case null: ['.hx']; - case v: v.split(',').map(s -> '.$s'); - } - final resExtension = file.substring(file.lastIndexOf('.')); - if (extensions.contains(resExtension)) { - for (exclude in excludes) { - if (isSubOf(FileSystem.absolutePath(file), exclude)) - return; - } - build(); - } - case Error(e): - } - ); - case Error(e): fail('Could not watch $path', e); + next = timer; + timer.start(() -> { + if (building) { + build(); + return; + } + building = true; + final start = Sys.time(); + if (Context.defined('watch.verbose')) + Sys.println('\x1b[32m> Build started\x1b[39m'); + server.build(config, (hasError: Bool) -> { + building = false; + final duration = (Sys.time() - start) * 1000; + closeRun(() -> { + closeRun = cb -> cb(); + timer.close(() -> { + if (Context.defined('watch.verbose')) { + final status = if (hasError) 31 else 32; + Sys.println('\x1b[${status}m> Build finished\x1b[39m'); + } + if (hasError) { + Sys.println('\x1b[90m> Found errors\x1b[39m'); + } else { + Sys.println('\x1b[36m> Build completed in ${formatDuration(duration)}\x1b[39m'); + switch Context.definedValue('watch.run') { + case null: + case v: closeRun = runCommand(v); + } + } + }); + }); + }); + }, 100); + case Error(e): fail('Could not init time', e); + } + } + function watch() { + for (path in paths) { + switch FsEvent.init(loop) { + case Ok(watcher): + watcher.start(path, [ + FsEventFlag.FS_EVENT_RECURSIVE + ], res -> + switch res { + case Ok({file: (_.toString()) => file}): + final extensions = switch Context.definedValue('watch.extensions') { + case null: ['.hx']; + case v: v.split(',').map(s -> '.$s'); + } + final resExtension = file.substring(file.lastIndexOf('.')); + if (extensions.contains(resExtension)) { + for (exclude in excludes) { + if (isSubOf(FileSystem.absolutePath(file), exclude)) + return; + } + build(); } + case Error(e): } - } - switch Idle.init(loop) { - case Ok(idle): idle.start(() -> { - if (isDone) { - idle.stop(); - build(); - watch(); - } - }); - case Error(e): fail('Could not get paths', e); - } - }); + ); + case Error(e): fail('Could not watch $path', e); + } + } + } + build(); + watch(); }); - loop.loop(); -} + }); + loop.loop(); +} \ No newline at end of file From 02e381c3df7e2c63bcac4878bc45846742a8ec20 Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Thu, 31 Oct 2024 10:17:57 +0100 Subject: [PATCH 08/11] Add missing function to get childDirs --- src/watch/Watch.hx | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/watch/Watch.hx b/src/watch/Watch.hx index 5d92028..cacdef1 100644 --- a/src/watch/Watch.hx +++ b/src/watch/Watch.hx @@ -254,6 +254,27 @@ function getFreePort(done: (port: haxe.ds.Option) -> Void) { } } +function childDirs(path:String, dirs:Array, cb:(dirs:Array, done:Bool) -> Void) { + final numDirs = dirs.length; + Dir.scan(loop, path, result -> { + switch result { + case Ok(dirScan): + var dirent:Dirent = dirScan.next(); + while (dirent != null) { + if (dirent.kind == DirentKind.DIR) { + final d = '${path}/${dirent.name.toString()}'; + dirs.push(d); + childDirs(d, dirs, cb); + } + dirent = dirScan.next(); + } + cb(dirs, dirs.length == numDirs); + case Error(e): + fail('Could not read child dir of $path', e); + } + }); +} + function register() { function getPort(done: (port: Int) -> Void) { switch Context.definedValue('watch.port') { From 8879e13ab134804e8d57243ff5de3477c5eefc3e Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Thu, 31 Oct 2024 10:25:10 +0100 Subject: [PATCH 09/11] Fix imports --- src/watch/Watch.hx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/watch/Watch.hx b/src/watch/Watch.hx index cacdef1..9e1042d 100644 --- a/src/watch/Watch.hx +++ b/src/watch/Watch.hx @@ -1,14 +1,19 @@ package watch; import eval.NativeString; -import eval.luv.Timer; +import eval.luv.Dir; import eval.luv.FsEvent; +import eval.luv.Idle; import eval.luv.Process; +import eval.luv.Result; import eval.luv.SockAddr; import eval.luv.Tcp; -import haxe.macro.Context; +import eval.luv.Timer; +import haxe.ds.Option; import haxe.io.Path; +import haxe.macro.Context; import sys.FileSystem; + using StringTools; using Lambda; From 804f4c0a71b0e8bcb94c7d771386168b49c7a416 Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Thu, 31 Oct 2024 10:25:46 +0100 Subject: [PATCH 10/11] Fix imports --- .haxerc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.haxerc b/.haxerc index cf76fd6..ef43126 100644 --- a/.haxerc +++ b/.haxerc @@ -1,4 +1,4 @@ { - "version": "4.2.3", + "version": "4.3.6", "resolveLibs": "scoped" } \ No newline at end of file From ab35612cfde93fd707410b491208c029b98a8e5b Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Thu, 31 Oct 2024 10:26:47 +0100 Subject: [PATCH 11/11] Fix conflict --- src/watch/Watch.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/watch/Watch.hx b/src/watch/Watch.hx index 9e1042d..9ab5cb6 100644 --- a/src/watch/Watch.hx +++ b/src/watch/Watch.hx @@ -14,8 +14,8 @@ import haxe.io.Path; import haxe.macro.Context; import sys.FileSystem; -using StringTools; using Lambda; +using StringTools; private final loop = sys.thread.Thread.current().events;