From d25e213aa8327b21d49ed6b3f4f5a6e714a7ff8a Mon Sep 17 00:00:00 2001 From: Delyan Angelov Date: Thu, 27 Jul 2023 09:50:26 +0300 Subject: [PATCH] tools, examples: add `--only-watch=*.v` option to `v watch` (#18974) --- cmd/tools/vwatch.v | 95 ++++++++++++++++++++++----- examples/vwatch/cli_clock/main.v | 14 ++++ examples/vwatch/web_server/.gitignore | 4 ++ examples/vwatch/web_server/main.v | 65 ++++++++++++++++++ vlib/v/help/common/watch.txt | 16 +++-- 5 files changed, 172 insertions(+), 22 deletions(-) create mode 100644 examples/vwatch/cli_clock/main.v create mode 100644 examples/vwatch/web_server/.gitignore create mode 100644 examples/vwatch/web_server/main.v diff --git a/cmd/tools/vwatch.v b/cmd/tools/vwatch.v index 016f6b92d8..aa28861fed 100644 --- a/cmd/tools/vwatch.v +++ b/cmd/tools/vwatch.v @@ -95,6 +95,7 @@ mut: ignore_exts []string // extensions of files that will be ignored, even if they change (useful for sqlite.db files for example) cmd_before_run string // a command to run before each re-run cmd_after_run string // a command to run after each re-run + only_watch []string // If not empty, *all* files that trigger updates, should match *at least one* of these s.match_glob() patterns. This is also triggered for vweb apps, to monitor for just *.v,*.js,*.css,*.html in vweb projects. } [if debug_vwatch ?] @@ -106,6 +107,33 @@ fn (context &Context) str() string { return 'Context{ pid: ${context.pid}, is_worker: ${context.is_worker}, check_period_ms: ${context.check_period_ms}, vexe: ${context.vexe}, opts: ${context.opts}, is_exiting: ${context.is_exiting}, vfiles: ${context.vfiles}' } +fn (mut context Context) is_ext_ignored(pf string, pf_ext string) bool { + for ipattern in context.ignore_exts { + if pf_ext.match_glob(ipattern) { + return true + } + } + if pf_ext in ['', '.so', '.a'] { + // on unix, the executables saved by compilers, usually do not have extensions at all, and shared libs are .so + return true + } + if pf_ext in ['.exe', '.dll', '.def'] { + // on windows, files with these extensions will be generated by the compiler + return true + } + // ignore common backup files saved by editors like emacs/jed/vim: + if pf_ext == '.bak' { + return true + } + if pf.starts_with('.#') { + return true + } + if pf.ends_with('~') { + return true + } + return false +} + fn (mut context Context) get_stats_for_affected_vfiles() []VFileStat { if context.affected_paths.len == 0 { mut apaths := map[string]bool{} @@ -121,34 +149,63 @@ fn (mut context Context) get_stats_for_affected_vfiles() []VFileStat { vfiles := os.execute(cmd) if vfiles.exit_code == 0 { paths_trimmed := vfiles.output.trim_space() - reported_used_files := paths_trimmed.split('\n') + reported_used_files := paths_trimmed.split_any('\n') $if trace_reported_used_files ? { context.elog('reported_used_files: ${reported_used_files}') } paths << reported_used_files } + mut is_vweb_found := false for vf in paths { apaths[os.real_path(os.dir(vf))] = true + if vf.contains('vweb.v') { + is_vweb_found = true + } + } + + if is_vweb_found { + if !os.args.any(it.starts_with('--only-watch')) { + // vweb is often used with SQLite .db or .sqlite3 files right next to the executable/source, + // that are updated by the vweb app, causing a restart of the app, which in turn causes the + // browser to reload the current page, that probably triggered the update in the first place. + // Note that the problem is not specific to SQLite, any database that stores its files in the + // current (project) folder, will also cause this. + println('`v watch` detected that you are compiling a vweb project.') + println(' Because of that, the `--only-watch=*.v,*.html,*.css,*.js` flag was also implied.') + println(' In result, `v watch` will ignore changes to other files.') + println(' Add your own --only-watch filter, if you wish to override that choice.') + println('') + context.only_watch = '*.v,*.html,*.css,*.js'.split_any(',') + } } context.affected_paths = apaths.keys() // context.elog('vfiles paths to be scanned: $context.affected_paths') } - // scan all files in the found folders + // scan all files in the found folders: mut newstats := []VFileStat{} for path in context.affected_paths { mut files := os.ls(path) or { []string{} } - for pf in files { + next_file: for pf in files { + pf_path := os.join_path_single(path, pf) + if context.only_watch.len > 0 { + // in the whitelist mode, first only allow files, which match at least one of the patterns in context.only_watch: + mut matched_pattern_idx := -1 + for ow_pattern_idx, ow_pattern in context.only_watch { + if pf_path.match_glob(ow_pattern) { + matched_pattern_idx = ow_pattern_idx + context.elog('> ${@METHOD} matched --only-watch pattern: ${ow_pattern}, for file: ${pf_path}') + break + } + } + if matched_pattern_idx == -1 { + context.elog('> ${@METHOD} --only-watch ignored file: ${pf_path}') + continue + } + } + // by default allow everything, except very specific extensions (backup files, executables etc): pf_ext := os.file_ext(pf).to_lower() - if pf_ext in ['', '.bak', '.exe', '.dll', '.so', '.def'] { - continue - } - if pf_ext in context.ignore_exts { - continue - } - if pf.starts_with('.#') { - continue - } - if pf.ends_with('~') { + if context.is_ext_ignored(pf, pf_ext) { + context.elog('> ${@METHOD} ignored extension: ${pf_ext}, for file: ${pf_path}') continue } f := os.join_path(path, pf) @@ -323,19 +380,21 @@ fn main() { // Options after `run` should be ignored, since they are intended for the user program, not for the watcher. // For example, `v watch run x.v -a -b -k', should pass all of -a -b -k to the compiled and run program. only_watch_options, has_run := all_before('run', all_args_after_watch_cmd) + mut fp := flag.new_flag_parser(only_watch_options) fp.application('v watch') fp.version('0.0.2') fp.description('Collect all .v files needed for a compilation, then re-run the compilation when any of the source changes.') fp.arguments_description('[--silent] [--clear] [--ignore .db] [--add /path/to/a/file.v] [run] program.v') fp.allow_unknown_args() - fp.limit_free_args_to_at_least(1)! + context.is_worker = fp.bool('vwatchworker', 0, false, 'Internal flag. Used to distinguish vwatch manager and worker processes.') context.silent = fp.bool('silent', `s`, false, 'Be more silent; do not print the watch timestamp before each re-run.') context.clear_terminal = fp.bool('clear', `c`, false, 'Clears the terminal before each re-run.') context.keep_running = fp.bool('keep', `k`, false, 'Keep the program running. Restart it automatically, if it exits by itself. Useful for gg/ui apps.') - context.add_files = fp.string('add', `a`, '', 'Add more files to be watched. Useful with `v watch -add=/tmp/feature.v run cmd/v /tmp/feature.v`, if you change *both* the compiler, and the feature.v file.').split(',') - context.ignore_exts = fp.string('ignore', `i`, '', 'Ignore files having these extensions. Useful with `v watch -ignore=.db run server.v`, if your server writes to an sqlite.db file in the same folder.').split(',') + context.add_files = fp.string('add', `a`, '', 'Add more files to be watched. Useful with `v watch --add=/tmp/feature.v run cmd/v /tmp/feature.v`, if you change *both* the compiler, and the feature.v file.').split_any(',') + context.ignore_exts = fp.string('ignore', `i`, '', 'Ignore files having these extensions. Useful with `v watch --ignore=.db run server.v`, if your server writes to an sqlite.db file in the same folder.').split_any(',') + context.only_watch = fp.string('only-watch', `o`, '', 'Watch only files matching these globe patterns. Example for a markdown renderer project: `v watch --only-watch=*.v,*.md run .`').split_any(',') show_help := fp.bool('help', `h`, false, 'Show this help screen.') context.cmd_before_run = fp.string('before', 0, '', 'A command to execute *before* each re-run.') context.cmd_after_run = fp.string('after', 0, '', 'A command to execute *after* each re-run.') @@ -351,6 +410,7 @@ fn main() { context.opts << all_args_before_watch_cmd context.opts << remaining_options if has_run { + context.opts << 'run' context.opts << all_after('run', all_args_after_watch_cmd) } context.elog('>>> context.pid: ${context.pid}') @@ -360,6 +420,7 @@ fn main() { context.elog('>>> context.clear_terminal: ${context.clear_terminal}') context.elog('>>> context.add_files: ${context.add_files}') context.elog('>>> context.ignore_exts: ${context.ignore_exts}') + context.elog('>>> context.only_watch: ${context.only_watch}') if context.is_worker { context.worker_main() } else { @@ -407,7 +468,7 @@ fn all_before(needle string, all []string) ([]string, bool) { if needle_pos == -1 { return all, false } - return all#[..needle_pos + 1], true + return all#[..needle_pos], true } fn all_after(needle string, all []string) []string { diff --git a/examples/vwatch/cli_clock/main.v b/examples/vwatch/cli_clock/main.v new file mode 100644 index 0000000000..77d40842ea --- /dev/null +++ b/examples/vwatch/cli_clock/main.v @@ -0,0 +1,14 @@ +import time +// This example demonstrates how to use `v watch` for simple CLI apps. + +fn main() { + println('Run with: `v watch run examples/vwatch/cli_clock`,') + println('then modify timer.v in your editor.') + println('The application will be restarted,') + println('as soon as you save your changes.') + println('') + for { + println('The time is now: ${time.now()}') + time.sleep(1000 * time.millisecond) + } +} diff --git a/examples/vwatch/web_server/.gitignore b/examples/vwatch/web_server/.gitignore new file mode 100644 index 0000000000..7031ed7df6 --- /dev/null +++ b/examples/vwatch/web_server/.gitignore @@ -0,0 +1,4 @@ +* +!.gitignore +!main.v + diff --git a/examples/vwatch/web_server/main.v b/examples/vwatch/web_server/main.v new file mode 100644 index 0000000000..2c17088e36 --- /dev/null +++ b/examples/vwatch/web_server/main.v @@ -0,0 +1,65 @@ +module main + +// This example demonstrates how to use `v watch` for vweb apps, that use sqlite .db files. +// +// Note 1: while developing services, it is useful to also add the `--keep` option of `v watch`, +// which will restart the app right away, even when it exits on its own. +// +// Note 2: vweb supports a special live reload mode, where it will make the browser to check for server +// restarts, and it will trigger a refresh of the current page, right after that is detected. +// +// The above means, that to get the most optimal prototyping experience for vweb apps, use: +// `v -d vweb_livereload watch --only-watch=*.v,*.html,*.css,*.js --keep run .` +import os +import vweb +import db.sqlite + +fn mydb() !sqlite.DB { + return sqlite.connect(os.resource_abs_path('app.db')) +} + +struct State { +mut: + counter int +} + +struct App { + vweb.Context +mut: + state shared State +} + +pub fn (mut app App) index() vweb.Result { + mut c := 0 + lock app.state { + app.state.counter++ + c = app.state.counter + } + visits := app.update_db() or { 0 } + return app.html(' +
Current request counter, after the server restart: ${c}. +
Total stored visits: ${visits} + ') +} + +fn (mut app App) update_db() !int { + mut db := mydb()! + db.exec('INSERT INTO visits (created_at) VALUES ("")') + visits := db.q_int('SELECT count(*) FROM visits') + db.close()! + return visits +} + +fn main() { + println('App demonstrating the use of `vweb` & `db.sqlite` together.') + println('For best prototyping experience, run with:') + println('`v -d vweb_livereload watch --keep run examples/vwatch/web_server/`') + println('') + mut db := mydb()! + db.exec('CREATE TABLE visits (id integer primary key AUTOINCREMENT, created_at timestamp default current_timestamp);') + db.exec('CREATE TRIGGER INSERT_visits AFTER INSERT ON visits BEGIN + UPDATE visits SET created_at = datetime("now", "localtime") WHERE rowid = new.rowid ; + END') + db.close()! + vweb.run(&App{}, 19123) +} diff --git a/vlib/v/help/common/watch.txt b/vlib/v/help/common/watch.txt index 16d6aa01d4..c409db7c66 100644 --- a/vlib/v/help/common/watch.txt +++ b/vlib/v/help/common/watch.txt @@ -13,22 +13,28 @@ Options: -c, --clear Clears the terminal before each re-run. - -a, --add Add more files to be watched. + -a, --add Add more files to be watched (separated by ,). Useful with `v watch -add=feature.v run cmd/v feature.v`, when you want to change *both* the V compiler, and the `feature.v` file. - -i, --ignore Ignore files having these extensions. + -i, --ignore Ignore files having these extensions (separated by ,) Useful with `v watch -ignore=.db run vwebserver.v`, if your `vwebserver` writes to an sqlite.db file in the same folder. + -o, --only-watch + Watch only files matching these glob patterns + The patterns are separated by `,`. Example for + a markdown renderer project: + v watch --only-watch=*.v,*.md run . + --before A command to execute *before* each re-run. Example: --before 'v wipe-cache' - + --after A command to execute *after* each re-run. Example: --after 'rm -rf /tmp/v/' - + You can also customise the timeout, after `v watch` will re-start a monitored -program automatically, even if it was not changed by setting the enviroment +program automatically, even if it was not changed by setting the enviroment variable VWATCH_TIMEOUT (in seconds). By default, it is 5 min. (300 seconds).