fortrangoingonforty/fgof-watch / cc12960

Browse files

Fix watch transport

Authored by espadonne
SHA
cc129606e6155a6ac4b141d0eb5fa5f6d5872028
Parents
e942851
Tree
9570725

6 changed files

StatusFile+-
M src/fgof_watch.f90 116 87
M src/fgof_watch_poll.c 50 23
M src/fgof_watch_types.f90 1 0
M test/test_watch_filters.f90 29 1
A test/test_watch_literal_paths.f90 47 0
M test/watch_test_support.f90 23 0
src/fgof_watch.f90modified
@@ -160,8 +160,10 @@ contains
160160
 
161161
     width = max(1, max_string_length(prefixes))
162162
     allocate(character(len=width) :: options%ignore_prefixes(size(prefixes)))
163
+    allocate(options%ignore_prefix_lengths(size(prefixes)))
163164
     do i = 1, size(prefixes)
164165
       options%ignore_prefixes(i) = prefixes(i)
166
+      options%ignore_prefix_lengths(i) = len(prefixes(i))
165167
     end do
166168
   end subroutine set_ignore_prefixes
167169
 
@@ -171,6 +173,9 @@ contains
171173
     if (allocated(options%ignore_prefixes)) then
172174
       deallocate(options%ignore_prefixes)
173175
     end if
176
+    if (allocated(options%ignore_prefix_lengths)) then
177
+      deallocate(options%ignore_prefix_lengths)
178
+    end if
174179
   end subroutine clear_ignore_prefixes
175180
 
176181
   subroutine collect_snapshot(root, options, entries, status_code, status_message)
@@ -187,7 +192,6 @@ contains
187192
     integer(c_int) :: prefix_count
188193
     integer(c_int) :: prefix_stride
189194
     character(kind=c_char), pointer :: raw_chars(:)
190
-    character(len=:), allocatable :: text
191195
 
192196
     c_root = to_c_string(root)
193197
     call pack_ignore_prefixes(options, prefix_count, prefix_stride, c_prefixes)
@@ -221,10 +225,9 @@ contains
221225
     end if
222226
 
223227
     call c_f_pointer(raw_ptr, raw_chars, [int(raw_len)])
224
-    text = buffer_to_text(raw_chars, int(raw_len))
228
+    call parse_snapshot_buffer(raw_chars, int(raw_len), entries)
225229
     call fgof_watch_free_buffer_c(raw_ptr)
226230
 
227
-    call parse_snapshot_text(text, entries)
228231
     call filter_entries(root, options, entries)
229232
     call sort_entries(entries)
230233
   end subroutine collect_snapshot
@@ -261,10 +264,10 @@ contains
261264
 
262265
     do i = 1, size(options%ignore_prefixes)
263266
       offset = (i - 1) * width
264
-      do j = 1, len_trim(options%ignore_prefixes(i))
267
+      do j = 1, ignore_prefix_length(options, i)
265268
         buffer(offset + j) = options%ignore_prefixes(i)(j:j)
266269
       end do
267
-      buffer(offset + len_trim(options%ignore_prefixes(i)) + 1) = c_null_char
270
+      buffer(offset + ignore_prefix_length(options, i) + 1) = c_null_char
268271
     end do
269272
   end subroutine pack_ignore_prefixes
270273
 
@@ -306,20 +309,22 @@ contains
306309
     type(watch_options), intent(in) :: options
307310
     character(len=*), intent(in) :: path
308311
     integer :: i
309
-    character(len=:), allocatable :: prefix
312
+    integer :: prefix_len
310313
 
311314
     matches = .false.
312315
     if (.not. allocated(options%ignore_prefixes)) return
313316
 
314317
     do i = 1, size(options%ignore_prefixes)
315
-      prefix = trim(options%ignore_prefixes(i))
316
-      if (len(prefix) == 0) cycle
317
-      if (path == prefix) then
318
-        matches = .true.
319
-        return
318
+      prefix_len = ignore_prefix_length(options, i)
319
+      if (prefix_len == 0) cycle
320
+      if (len(path) == prefix_len) then
321
+        if (path == options%ignore_prefixes(i)(1:prefix_len)) then
322
+          matches = .true.
323
+          return
324
+        end if
320325
       end if
321
-      if (len(path) > len(prefix)) then
322
-        if (path(1:len(prefix)) == prefix .and. path(len(prefix) + 1:len(prefix) + 1) == "/") then
326
+      if (len(path) > prefix_len) then
327
+        if (path(1:prefix_len) == options%ignore_prefixes(i)(1:prefix_len) .and. path(prefix_len + 1:prefix_len + 1) == "/") then
323328
           matches = .true.
324329
           return
325330
         end if
@@ -327,6 +332,17 @@ contains
327332
     end do
328333
   end function path_matches_ignore_prefix
329334
 
335
+  integer function ignore_prefix_length(options, index_value) result(length_value)
336
+    type(watch_options), intent(in) :: options
337
+    integer, intent(in) :: index_value
338
+
339
+    if (allocated(options%ignore_prefix_lengths)) then
340
+      length_value = options%ignore_prefix_lengths(index_value)
341
+    else
342
+      length_value = len_trim(options%ignore_prefixes(index_value))
343
+    end if
344
+  end function ignore_prefix_length
345
+
330346
   function path_after_root(root, path) result(relative)
331347
     character(len=*), intent(in) :: root
332348
     character(len=*), intent(in) :: path
@@ -752,105 +768,118 @@ contains
752768
     if (previous_entry%is_directory .neqv. current_entry%is_directory) changed = .true.
753769
   end function entry_changed
754770
 
755
-  subroutine parse_snapshot_text(text, entries)
756
-    character(len=*), intent(in) :: text
771
+  subroutine parse_snapshot_buffer(buffer, count, entries)
772
+    character(kind=c_char), intent(in) :: buffer(:)
773
+    integer, intent(in) :: count
757774
     type(watch_entry), allocatable, intent(out) :: entries(:)
758
-    integer :: count
759775
     integer :: i
776
+    integer :: field_count
777
+    integer :: record_count
760778
     integer :: start
761
-    integer :: n
779
+    integer :: terminator_index
780
+    character(len=:), allocatable :: kind_text
781
+    character(len=:), allocatable :: inode_text
782
+    character(len=:), allocatable :: size_text
783
+    character(len=:), allocatable :: mtime_sec_text
784
+    character(len=:), allocatable :: mtime_nsec_text
785
+    character(len=:), allocatable :: path_text
762786
 
763
-    n = len(text)
764
-    if (n == 0) then
787
+    if (count <= 0) then
765788
       allocate(entries(0))
766789
       return
767790
     end if
768791
 
769
-    count = 0
770
-    do i = 1, n
771
-      if (text(i:i) == new_line("a")) count = count + 1
792
+    field_count = 0
793
+    do i = 1, count
794
+      if (buffer(i) == c_null_char) field_count = field_count + 1
772795
     end do
773
-    if (text(n:n) /= new_line("a")) count = count + 1
796
+    if (field_count == 0 .or. mod(field_count, 6) /= 0) then
797
+      allocate(entries(0))
798
+      return
799
+    end if
774800
 
775
-    allocate(entries(count))
776
-    count = 0
801
+    record_count = field_count / 6
802
+    allocate(entries(record_count))
777803
     start = 1
778
-    do i = 1, n
779
-      if (text(i:i) /= new_line("a")) cycle
780
-      count = count + 1
781
-      call parse_snapshot_line(text(start:i - 1), entries(count))
782
-      start = i + 1
804
+    do i = 1, record_count
805
+      call next_nul_field(buffer, count, start, kind_text, terminator_index)
806
+      if (terminator_index == 0) exit
807
+      call next_nul_field(buffer, count, start, inode_text, terminator_index)
808
+      if (terminator_index == 0) exit
809
+      call next_nul_field(buffer, count, start, size_text, terminator_index)
810
+      if (terminator_index == 0) exit
811
+      call next_nul_field(buffer, count, start, mtime_sec_text, terminator_index)
812
+      if (terminator_index == 0) exit
813
+      call next_nul_field(buffer, count, start, mtime_nsec_text, terminator_index)
814
+      if (terminator_index == 0) exit
815
+      call next_nul_field(buffer, count, start, path_text, terminator_index)
816
+      if (terminator_index == 0) exit
817
+      call parse_snapshot_fields(kind_text, inode_text, size_text, mtime_sec_text, mtime_nsec_text, path_text, entries(i))
783818
     end do
784
-
785
-    if (start <= n) then
786
-      count = count + 1
787
-      call parse_snapshot_line(text(start:n), entries(count))
788
-    end if
789
-  end subroutine parse_snapshot_text
790
-
791
-  subroutine parse_snapshot_line(line, entry)
792
-    character(len=*), intent(in) :: line
819
+  end subroutine parse_snapshot_buffer
820
+
821
+  subroutine parse_snapshot_fields(kind_text, inode_text, size_text, mtime_sec_text, mtime_nsec_text, path_text, entry)
822
+    character(len=*), intent(in) :: kind_text
823
+    character(len=*), intent(in) :: inode_text
824
+    character(len=*), intent(in) :: size_text
825
+    character(len=*), intent(in) :: mtime_sec_text
826
+    character(len=*), intent(in) :: mtime_nsec_text
827
+    character(len=*), intent(in) :: path_text
793828
     type(watch_entry), intent(out) :: entry
794
-    integer :: tab1
795
-    integer :: tab2
796
-    integer :: tab3
797
-    integer :: tab4
798
-    integer :: tab5
799
-
800
-    tab1 = index(line, achar(9))
801
-    tab2 = next_tab(line, tab1 + 1)
802
-    tab3 = next_tab(line, tab2 + 1)
803
-    tab4 = next_tab(line, tab3 + 1)
804
-    tab5 = next_tab(line, tab4 + 1)
805
-
806
-    if (tab1 <= 0 .or. tab2 <= 0 .or. tab3 <= 0 .or. tab4 <= 0 .or. tab5 <= 0) then
807
-      entry = watch_entry()
808
-      entry%path = ""
809
-      return
810
-    end if
811
-
812
-    entry%is_directory = (line(1:1) == "D")
813
-    read(line(tab1 + 1:tab2 - 1), *) entry%inode
814
-    read(line(tab2 + 1:tab3 - 1), *) entry%size
815
-    read(line(tab3 + 1:tab4 - 1), *) entry%mtime_sec
816
-    read(line(tab4 + 1:tab5 - 1), *) entry%mtime_nsec
817
-    entry%path = line(tab5 + 1:)
818
-  end subroutine parse_snapshot_line
829
+    integer :: iostat_value
819830
 
820
-  integer function next_tab(line, start_index) result(position)
821
-    character(len=*), intent(in) :: line
822
-    integer, intent(in) :: start_index
823
-    integer :: offset
824
-
825
-    if (start_index > len(line)) then
826
-      position = 0
831
+    entry = watch_entry()
832
+    if (len(kind_text) == 0) then
833
+      entry%path = ""
827834
       return
828835
     end if
829836
 
830
-    offset = index(line(start_index:), achar(9))
831
-    if (offset == 0) then
832
-      position = 0
833
-    else
834
-      position = start_index + offset - 1
835
-    end if
836
-  end function next_tab
837
-
838
-  function buffer_to_text(buffer, count) result(text)
837
+    entry%is_directory = (kind_text(1:1) == "D")
838
+    read(inode_text, *, iostat=iostat_value) entry%inode
839
+    if (iostat_value /= 0) entry%inode = 0
840
+    read(size_text, *, iostat=iostat_value) entry%size
841
+    if (iostat_value /= 0) entry%size = 0
842
+    read(mtime_sec_text, *, iostat=iostat_value) entry%mtime_sec
843
+    if (iostat_value /= 0) entry%mtime_sec = 0
844
+    read(mtime_nsec_text, *, iostat=iostat_value) entry%mtime_nsec
845
+    if (iostat_value /= 0) entry%mtime_nsec = 0
846
+    entry%path = path_text
847
+  end subroutine parse_snapshot_fields
848
+
849
+  subroutine next_nul_field(buffer, count, start_index, field, terminator_index)
839850
     character(kind=c_char), intent(in) :: buffer(:)
840851
     integer, intent(in) :: count
841
-    character(len=:), allocatable :: text
852
+    integer, intent(inout) :: start_index
853
+    character(len=:), allocatable, intent(out) :: field
854
+    integer, intent(out) :: terminator_index
842855
     integer :: i
856
+    integer :: width
843857
 
844
-    if (count <= 0) then
845
-      text = ""
858
+    if (start_index > count) then
859
+      field = ""
860
+      terminator_index = 0
846861
       return
847862
     end if
848863
 
849
-    allocate(character(len=count) :: text)
850
-    do i = 1, count
851
-      text(i:i) = char(iachar(buffer(i)))
864
+    terminator_index = 0
865
+    do i = start_index, count
866
+      if (buffer(i) == c_null_char) then
867
+        terminator_index = i
868
+        exit
869
+      end if
870
+    end do
871
+    if (terminator_index == 0) then
872
+      field = ""
873
+      return
874
+    end if
875
+
876
+    width = terminator_index - start_index
877
+    allocate(character(len=width) :: field)
878
+    do i = 1, width
879
+      field(i:i) = char(iachar(buffer(start_index + i - 1)))
852880
     end do
853
-  end function buffer_to_text
881
+    start_index = terminator_index + 1
882
+  end subroutine next_nul_field
854883
 
855884
   function errno_message(prefix, errnum) result(message)
856885
     character(len=*), intent(in) :: prefix
src/fgof_watch_poll.cmodified
@@ -138,12 +138,38 @@ static int fgof_watch_append_text(fgof_watch_buffer *buffer, const char *text, s
138138
     return 0;
139139
 }
140140
 
141
+static int fgof_watch_append_field(fgof_watch_buffer *buffer, const char *text, size_t text_len) {
142
+    static const char nul = '\0';
143
+    int status;
144
+
145
+    status = fgof_watch_append_text(buffer, text, text_len);
146
+    if (status != 0) {
147
+        return status;
148
+    }
149
+
150
+    return fgof_watch_append_text(buffer, &nul, 1);
151
+}
152
+
153
+static int fgof_watch_append_int64_field(fgof_watch_buffer *buffer, long long value) {
154
+    char text[64];
155
+    int text_len;
156
+
157
+    text_len = snprintf(text, sizeof(text), "%lld", value);
158
+    if (text_len < 0) {
159
+        return EINVAL;
160
+    }
161
+    if ((size_t)text_len >= sizeof(text)) {
162
+        return EOVERFLOW;
163
+    }
164
+
165
+    return fgof_watch_append_field(buffer, text, (size_t)text_len);
166
+}
167
+
141168
 static int fgof_watch_append_entry(fgof_watch_buffer *buffer, const char *path, const struct stat *st) {
142
-    char line[4096];
143169
     char kind;
144
-    int line_len;
145170
     long long mtime_sec;
146171
     long long mtime_nsec;
172
+    int status;
147173
 
148174
     if (S_ISDIR(st->st_mode)) {
149175
         kind = 'D';
@@ -159,26 +185,28 @@ static int fgof_watch_append_entry(fgof_watch_buffer *buffer, const char *path,
159185
     mtime_nsec = (long long)st->st_mtim.tv_nsec;
160186
 #endif
161187
 
162
-    line_len = snprintf(
163
-        line,
164
-        sizeof(line),
165
-        "%c\t%lld\t%lld\t%lld\t%lld\t%s\n",
166
-        kind,
167
-        (long long)st->st_ino,
168
-        (long long)st->st_size,
169
-        mtime_sec,
170
-        mtime_nsec,
171
-        path
172
-    );
173
-
174
-    if (line_len < 0) {
175
-        return EINVAL;
188
+    status = fgof_watch_append_field(buffer, &kind, 1);
189
+    if (status != 0) {
190
+        return status;
176191
     }
177
-    if ((size_t)line_len >= sizeof(line)) {
178
-        return EOVERFLOW;
192
+    status = fgof_watch_append_int64_field(buffer, (long long)st->st_ino);
193
+    if (status != 0) {
194
+        return status;
195
+    }
196
+    status = fgof_watch_append_int64_field(buffer, (long long)st->st_size);
197
+    if (status != 0) {
198
+        return status;
199
+    }
200
+    status = fgof_watch_append_int64_field(buffer, mtime_sec);
201
+    if (status != 0) {
202
+        return status;
203
+    }
204
+    status = fgof_watch_append_int64_field(buffer, mtime_nsec);
205
+    if (status != 0) {
206
+        return status;
179207
     }
180208
 
181
-    return fgof_watch_append_text(buffer, line, (size_t)line_len);
209
+    return fgof_watch_append_field(buffer, path, strlen(path));
182210
 }
183211
 
184212
 static int fgof_watch_visit(
@@ -195,6 +223,7 @@ static int fgof_watch_visit(
195223
     DIR *dirp;
196224
     struct dirent *entry;
197225
     struct stat st;
226
+    int status;
198227
 
199228
     if (fgof_watch_should_ignore(root, path, ignore_hidden, prefix_count, prefix_stride, prefixes)) {
200229
         return 0;
@@ -207,8 +236,8 @@ static int fgof_watch_visit(
207236
         return errno;
208237
     }
209238
 
210
-    if (fgof_watch_append_entry(buffer, path, &st) != 0) {
211
-        return -1;
239
+    if ((status = fgof_watch_append_entry(buffer, path, &st)) != 0) {
240
+        return status;
212241
     }
213242
 
214243
     if (!S_ISDIR(st.st_mode)) {
@@ -230,8 +259,6 @@ static int fgof_watch_visit(
230259
     while ((entry = readdir(dirp)) != NULL) {
231260
         char *child;
232261
         size_t child_len;
233
-        int status;
234
-
235262
         if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
236263
             continue;
237264
         }
src/fgof_watch_types.f90modified
@@ -29,6 +29,7 @@ module fgof_watch_types
2929
     logical :: ignore_hidden = .false.
3030
     logical :: emit_directory_events = .true.
3131
     character(len=:), allocatable :: ignore_prefixes(:)
32
+    integer, allocatable :: ignore_prefix_lengths(:)
3233
   end type watch_options
3334
 
3435
   type :: watch_entry
test/test_watch_filters.f90modified
@@ -7,11 +7,13 @@ program test_watch_filters
77
     expect_single_event, &
88
     make_dir, &
99
     remove_tree, &
10
+    touch_path, &
1011
     write_text
1112
   implicit none
1213
 
1314
   call test_ignore_hidden()
1415
   call test_ignore_prefixes()
16
+  call test_ignore_prefix_literal_spaces()
1517
   call test_directory_event_shaping()
1618
 
1719
 contains
@@ -56,7 +58,7 @@ contains
5658
     call set_ignore_prefixes(options, [character(len=len(ignored_dir)) :: ignored_dir])
5759
     call init_watch(session, root, options)
5860
 
59
-    call write_text(ignored_path, "alpha")
61
+    call touch_path(ignored_path)
6062
     events = poll_watch(session)
6163
     call expect_no_events(events, "ignored prefix should suppress nested file creation")
6264
 
@@ -71,6 +73,32 @@ contains
7173
     call remove_tree(root)
7274
   end subroutine test_ignore_prefixes
7375
 
76
+  subroutine test_ignore_prefix_literal_spaces()
77
+    character(len=*), parameter :: root = "build/watch-tests-prefix-literal-spaces"
78
+    character(len=:), allocatable :: ignored_path
79
+    character(len=*), parameter :: visible_path = "build/watch-tests-prefix-literal-spaces/main.txt"
80
+    type(watch_event), allocatable :: events(:)
81
+    type(watch_options) :: options
82
+    type(watch_session) :: session
83
+
84
+    call ensure_clean_dir(root)
85
+
86
+    ignored_path = root // "/skip "
87
+    call set_ignore_prefixes(options, [ignored_path])
88
+    call init_watch(session, root, options)
89
+
90
+    call touch_path(ignored_path)
91
+    events = poll_watch(session)
92
+    call expect_no_events(events, "ignore prefix should preserve trailing spaces literally")
93
+
94
+    call write_text(visible_path, "beta")
95
+    events = poll_watch(session)
96
+    call expect_single_event(events, FGOF_WATCH_EVT_CREATED, visible_path, "", .false., "visible file outside literal-space prefix should still be reported")
97
+
98
+    call reset_watch(session)
99
+    call remove_tree(root)
100
+  end subroutine test_ignore_prefix_literal_spaces
101
+
74102
   subroutine test_directory_event_shaping()
75103
     character(len=*), parameter :: root = "build/watch-tests-directory-events"
76104
     character(len=*), parameter :: child_dir = "build/watch-tests-directory-events/child"
test/test_watch_literal_paths.f90added
@@ -0,0 +1,47 @@
1
+program test_watch_literal_paths
2
+  use fgof_watch, only : init_watch, poll_watch, reset_watch
3
+  use fgof_watch_types, only : FGOF_WATCH_EVT_CREATED, watch_event, watch_session
4
+  use watch_test_support, only : ensure_clean_dir, expect_single_event, remove_tree, write_text
5
+  implicit none
6
+
7
+  call test_tab_name()
8
+  call test_newline_name()
9
+
10
+contains
11
+
12
+  subroutine test_tab_name()
13
+    character(len=*), parameter :: root = "build/watch-tests-tab-name"
14
+    character(len=:), allocatable :: tab_path
15
+    type(watch_event), allocatable :: events(:)
16
+    type(watch_session) :: session
17
+
18
+    call ensure_clean_dir(root)
19
+    call init_watch(session, root)
20
+
21
+    tab_path = root // "/tab" // achar(9) // "name.txt"
22
+    call write_text(tab_path, "alpha")
23
+    events = poll_watch(session)
24
+    call expect_single_event(events, FGOF_WATCH_EVT_CREATED, tab_path, "", .false., "tab characters in paths should round-trip through watch events")
25
+
26
+    call reset_watch(session)
27
+    call remove_tree(root)
28
+  end subroutine test_tab_name
29
+
30
+  subroutine test_newline_name()
31
+    character(len=*), parameter :: root = "build/watch-tests-newline-name"
32
+    character(len=:), allocatable :: newline_path
33
+    type(watch_event), allocatable :: events(:)
34
+    type(watch_session) :: session
35
+
36
+    call ensure_clean_dir(root)
37
+    call init_watch(session, root)
38
+
39
+    newline_path = root // "/line" // new_line("a") // "break.txt"
40
+    call write_text(newline_path, "alpha")
41
+    events = poll_watch(session)
42
+    call expect_single_event(events, FGOF_WATCH_EVT_CREATED, newline_path, "", .false., "newline characters in paths should round-trip through watch events")
43
+
44
+    call reset_watch(session)
45
+    call remove_tree(root)
46
+  end subroutine test_newline_name
47
+end program test_watch_literal_paths
test/watch_test_support.f90modified
@@ -12,6 +12,7 @@ module watch_test_support
1212
   public :: move_path
1313
   public :: remove_path
1414
   public :: remove_tree
15
+  public :: touch_path
1516
   public :: write_text
1617
 
1718
 contains
@@ -48,6 +49,12 @@ contains
4849
     call run_command("rm -f " // path)
4950
   end subroutine remove_path
5051
 
52
+  subroutine touch_path(path)
53
+    character(len=*), intent(in) :: path
54
+
55
+    call run_command("touch " // shell_quote(path))
56
+  end subroutine touch_path
57
+
5158
   subroutine move_path(source, destination)
5259
     character(len=*), intent(in) :: source
5360
     character(len=*), intent(in) :: destination
@@ -105,4 +112,20 @@ contains
105112
     if (exitstat /= 0) error stop "command failed: " // trim(command)
106113
   end subroutine run_command
107114
 
115
+  function shell_quote(text) result(quoted)
116
+    character(len=*), intent(in) :: text
117
+    character(len=:), allocatable :: quoted
118
+    integer :: i
119
+
120
+    quoted = "'"
121
+    do i = 1, len(text)
122
+      if (text(i:i) == "'") then
123
+        quoted = quoted // achar(39) // achar(34) // achar(39) // achar(34) // achar(39)
124
+      else
125
+        quoted = quoted // text(i:i)
126
+      end if
127
+    end do
128
+    quoted = quoted // "'"
129
+  end function shell_quote
130
+
108131
 end module watch_test_support