v2 / vlib / v / live / executable / reloader.c.v
199 lines · 183 sloc · 6.22 KB · 2332ecff4811b8c97dfda8e825170e9397962519
Raw
1module executable
2
3import os
4import time
5import dl
6import v.live
7
8// The live reloader code is implemented here.
9// Note: new_live_reload_info will be called by generated C code inside main()
10@[markused]
11pub fn new_live_reload_info(original string, vexe string, vopts string, live_fn_mutex voidptr, live_linkfn live.FNLinkLiveSymbols) &live.LiveReloadInfo {
12 file_base := os.file_name(original).replace('.v', '')
13 so_dir := os.cache_dir()
14 mut so_extension := dl.dl_ext
15 $if macos {
16 so_extension = '.dylib'
17 }
18 // $if msvc { so_extension = '.dll' } $else { so_extension = '.so' }
19 res := &live.LiveReloadInfo{
20 original: original
21 vexe: vexe
22 vopts: vopts
23 live_fn_mutex: live_fn_mutex
24 live_linkfn: live_linkfn
25 so_extension: so_extension
26 so_name_template: '${so_dir}/tmp.%d.${file_base}'
27 live_lib: 0
28 reloads: 0
29 reload_time_ms: 0
30 }
31 elog(res, @FN)
32 return res
33}
34
35// Note: start_reloader will be called by generated code inside main(), to start
36// the hot code reloader thread. start_reloader is executed in the context of
37// the original main thread.
38@[markused]
39pub fn start_reloader(mut r live.LiveReloadInfo) {
40 elog(r, @FN)
41 // The shared library should be loaded once in the main thread
42 // If that fails, the program would crash anyway, just provide
43 // an error message to the user and exit:
44 r.reloads++
45 compile_and_reload_shared_lib(mut r) or {
46 eprintln(err)
47 exit(1)
48 }
49 spawn reloader(mut r)
50}
51
52// add_live_monitored_file will be called by the generated code inside main(), to add a list of all the .v files
53// that were used during the main program compilation. Any change to any of them, will later trigger a
54// recompilation and reloading of the produced shared library. This makes it possible for @[live] functions
55// inside modules to also work, not just in the top level program.
56@[markused]
57pub fn add_live_monitored_file(mut lri live.LiveReloadInfo, path string) {
58 mtime := os.file_last_mod_unix(path)
59 lri.monitored_files << path
60 elog(lri, '${@FN} mtime: ${mtime:12} path: ${path}')
61 if lri.last_mod_ts < mtime {
62 lri.last_mod_ts = mtime
63 }
64}
65
66@[if debuglive ?]
67fn elog(r &live.LiveReloadInfo, s string) {
68 eprintln('> debuglive r: ${voidptr(r)} &g_live_reload_info: ${voidptr(&g_live_reload_info)} | g_live_reload_info: ${voidptr(g_live_reload_info)} ${s}')
69}
70
71fn compile_and_reload_shared_lib(mut r live.LiveReloadInfo) !bool {
72 sw := time.new_stopwatch()
73 new_lib_path := compile_lib(mut r) or { return error('errors while compiling ${r.original}') }
74 elog(r, '> compile_and_reload_shared_lib compiled: ${new_lib_path}')
75 load_lib(mut r, new_lib_path)
76 r.reload_time_ms = int(sw.elapsed().milliseconds())
77 return true
78}
79
80fn compile_lib(mut r live.LiveReloadInfo) ?string {
81 _, new_lib_path_with_extension := current_shared_library_path(mut r)
82 cmd := '${os.quoted_path(r.vexe)} ${r.vopts} -o ${os.quoted_path(new_lib_path_with_extension)} ${os.quoted_path(r.original)}'
83 elog(r, '> compilation cmd: ${cmd}')
84 cwatch := time.new_stopwatch()
85 recompilation_result := os.execute(cmd)
86 elog(r, 'compilation took: ${cwatch.elapsed().milliseconds()}ms')
87 if recompilation_result.exit_code != 0 {
88 eprintln('recompilation error:')
89 eprintln(recompilation_result.output)
90 return none
91 }
92 if !os.exists(new_lib_path_with_extension) {
93 eprintln('new_lib_path: ${new_lib_path_with_extension} does not exist')
94 return none
95 }
96 return new_lib_path_with_extension
97}
98
99fn current_shared_library_path(mut r live.LiveReloadInfo) (string, string) {
100 lib_path := r.so_name_template.replace('\\', '\\\\').replace('%d', r.reloads.str())
101 lib_path_with_extension := lib_path + r.so_extension
102 return lib_path, lib_path_with_extension
103}
104
105fn load_lib(mut r live.LiveReloadInfo, new_lib_path string) {
106 elog(r, 'live mutex locking...')
107 C.pthread_mutex_lock(r.live_fn_mutex)
108 elog(r, 'live mutex locked')
109
110 if r.cb_locked_before != unsafe { nil } {
111 r.cb_locked_before(r)
112 }
113
114 protected_load_lib(mut r, new_lib_path)
115
116 r.reloads_ok++
117 if r.cb_locked_after != unsafe { nil } {
118 r.cb_locked_after(r)
119 }
120
121 elog(r, 'live mutex unlocking...')
122 C.pthread_mutex_unlock(r.live_fn_mutex)
123 elog(r, 'live mutex unlocked')
124}
125
126fn protected_load_lib(mut r live.LiveReloadInfo, new_lib_path string) {
127 if r.live_lib != 0 {
128 dl.close(r.live_lib)
129 r.live_lib = C.NULL
130 }
131 r.live_lib = dl.open(new_lib_path, dl.rtld_lazy)
132 if r.live_lib == 0 {
133 eprintln('opening ${new_lib_path} failed')
134 exit(1)
135 }
136 r.live_linkfn(r.live_lib)
137 elog(r, '> load_lib OK, new live_lib: ${r.live_lib}')
138 // removing the .so file from the filesystem after dlopen-ing
139 // it is safe, since it will still be mapped in memory
140 os.rm(new_lib_path) or {}
141}
142
143// Note: r.reloader() is executed in a new, independent thread
144fn reloader(mut r live.LiveReloadInfo) {
145 // elog(r,'reloader, r: ${r}')
146 mut last_ts := r.last_mod_ts
147 mut monitored_file_paths := r.monitored_files.clone()
148 // it is much more likely that the user will be changing *the latest* files
149 // => put them first, so the search can be cut earlier:
150 monitored_file_paths.reverse_in_place()
151 for {
152 if r.cb_recheck != unsafe { nil } {
153 r.cb_recheck(r)
154 }
155 sw := time.new_stopwatch()
156 now_ts := get_latest_ts_from_monitored_files(monitored_file_paths, last_ts)
157 $if trace_check_monitored_files ? {
158 eprintln('check if last_ts: ${last_ts} < now_ts: ${now_ts} , took ${sw.elapsed().microseconds()} microseconds')
159 }
160 if last_ts < now_ts {
161 r.reloads++
162 last_ts = now_ts
163 r.last_mod_ts = last_ts
164 if r.cb_before != unsafe { nil } {
165 r.cb_before(r)
166 }
167 compile_and_reload_shared_lib(mut r) or {
168 if r.cb_compile_failed != unsafe { nil } {
169 r.cb_compile_failed(r)
170 }
171 if r.cb_after != unsafe { nil } {
172 r.cb_after(r)
173 }
174 continue
175 }
176 if r.cb_after != unsafe { nil } {
177 r.cb_after(r)
178 }
179 }
180 if r.recheck_period_ms > 0 {
181 time.sleep(r.recheck_period_ms * time.millisecond)
182 }
183 }
184}
185
186fn get_latest_ts_from_monitored_files(monitored_file_paths []string, last_ts i64) i64 {
187 mut latest_ts := i64(0)
188 for f in monitored_file_paths {
189 mtime := os.file_last_mod_unix(f)
190 if mtime > latest_ts {
191 latest_ts = mtime
192 if mtime > last_ts {
193 // no need to check further, since we already know, that there is a newer file, so return early its timestamp
194 return mtime
195 }
196 }
197 }
198 return latest_ts
199}
200