File Coverage

blib/lib/File/Unsaved.pm
Criterion Covered Total %
statement 36 48 75.0
branch 17 36 47.2
condition 3 10 30.0
subroutine 4 4 100.0
pod 1 1 100.0
total 61 99 61.6


line stmt bran cond sub pod time code
1             package File::Unsaved;
2              
3             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
4             our $DATE = '2020-09-30'; # DATE
5             our $DIST = 'File-Unsaved'; # DIST
6             our $VERSION = '0.070'; # VERSION
7              
8 1     1   40639 use 5.010001;
  1         9  
9 1     1   6 use strict;
  1         2  
  1         31  
10 1     1   6 use warnings;
  1         8  
  1         732  
11              
12             require Exporter;
13             our @ISA = qw(Exporter);
14             our @EXPORT_OK = qw(check_unsaved_file);
15              
16             our %SPEC;
17              
18             $SPEC{check_unsaved_file} = {
19             v => 1.1,
20             summary => 'Check whether file has unsaved modification in an editor',
21             description => <<'_',
22              
23             This function tries, using some heuristics, to find out if a file is being
24             opened and has unsaved modification in an editor. Currently the supported
25             editors are: Emacs, joe, vim.
26              
27             The heuristics are as folow:
28              
29             * Emacs, joe, mc: check whether `.#` symlink exists. Emacs targets the
30             symlink to `@.:` while joe and mc to
31             `@.`. Caveat: Unix only.
32              
33             * vim: check whether `..swp` file exists, not older than file, and its
34             0x03ef-th byte has the value of `U` (which vim uses to mark the file as
35             unsaved). Caveat: vim can be instructed to put swap file somewhere else or not
36             create swap file at all, so in those cases unsaved data will not be detected.
37              
38             _
39             args => {
40             path => {
41             schema => 'str*',
42             req => 1,
43             pos => 0,
44             },
45             check_pid => {
46             summary => 'Whether to check that PID is actually an editor',
47             schema => 'bool*',
48             default => 1,
49             description => <<'_',
50              
51             A temporary file might be stale, so checking the existence of temporary file is
52             not enough. If the temporary file provides pointer to a PID, and this setting is
53             set to true, will actually check that the PID exists.
54              
55             _
56             },
57             check_proc_name => {
58             summary => 'Whether to check that process name is actually the '.
59             'corresponding editor',
60             schema => 'bool*',
61             default => 1,
62             description => <<'_',
63              
64             Is activated only `check_pid` is also 1 and if `Proc::Find` (and thus
65             `Proc::ProcessTable`) is available.
66              
67             Might produce a false negative if you happen to rename the editor or use a
68             differently-named fork/derivative of said editor, although this should be rare.
69              
70             _
71             },
72             },
73             result_naked => 1,
74             result => {
75             schema => ['any*', of=>['bool*', 'hash*']],
76             description => <<'_',
77              
78             Return false if no unsaved data is detected, or else a hash structure. Hash will
79             contain these keys: `editor` (kind of editor, possible values: `emacs`,
80             `joe/mc`, `joe`, `mc`, `vim`) and might contain these keys: `pid` (PID of
81             editor), `user`, `host`, `timestamp`.
82              
83              
84             _
85             },
86             };
87             sub check_unsaved_file{
88 6     6 1 3401 require File::Spec;
89              
90 6         21 my %args = @_;
91 6   100     27 my $check_pid = $args{check_pid} // 1;
92 6   50     22 my $check_proc_name = $args{check_proc_name} // 1;
93 6         10 my $path = $args{path};
94              
95 6 100       109 (-f $path) or die "File does not exist or not a regular file: '$path'";
96              
97 5         87 my ($vol, $dir, $file) = File::Spec->splitpath($path);
98              
99             # emacs & joe/mc
100             CHECK1:
101             {
102 5         16 my $spath = File::Spec->catpath($vol, $dir, ".#$file");
  5         36  
103 5 100       78 last unless -l $spath;
104 2         20 my $target = readlink $spath;
105 2 100       20 if ($target =~ /\A(.+)\@(.+)\.(\d+):(\d+)\z/) {
    50          
106 1         16 my $res = {editor=>'emacs',
107             user=>$1, host=>$2, pid=>$3, timestamp=>$4};
108 1 50       5 if ($check_pid) {
109 0 0       0 last CHECK1 unless kill(0, $res->{pid});
110 0 0 0     0 if ($check_proc_name && eval {require Proc::Find; 1}) {
  0         0  
  0         0  
111             last CHECK1 unless Proc::Find::proc_exists(
112 0 0       0 pid => $res->{pid}, name => qr/\b(emacs)\b/,
113             );
114             }
115             }
116 1         11 return $res;
117             } elsif ($target =~ /\A(.+)\@(.+)\.(\d+)\z/) {
118 1         16 my $res = {editor=>'joe/mc',
119             user=>$1, host=>$2, pid=>$3};
120 1 50       9 if ($check_pid) {
121 0 0       0 last CHECK1 unless kill(0, $res->{pid});
122 0 0 0     0 if ($check_proc_name && eval {require Proc::Find; 1}) {
  0         0  
  0         0  
123             my $findres = Proc::Find::proc_find(
124 0         0 pid => $res->{pid}, name => qr/\b(joe|mc)\b/);
125 0 0       0 last CHECK1 unless $findres;
126 0         0 $res->{editor} = $findres->{name};
127             }
128             }
129 1         13 return $res;
130             }
131             }
132              
133             # vim
134             CHECK_VIM:
135             {
136 3         8 my $spath = File::Spec->catpath($vol, $dir, ".$file.swp");
  3         22  
137 3 100       43 last unless -f $spath;
138 2 50       39 last if (-M $spath) > (-M $path); # swap file is older
139 2 50       66 open my($fh), "<", $spath or last;
140 2 50       16 sysseek $fh, 0x03ef, 0 or last;
141 2 100       38 sysread $fh, my($data), 1 or last;
142 1 50       6 $data eq 'U' or last;
143 1         18 return {editor => 'vim'};
144             }
145              
146 2         16 undef;
147             }
148              
149             1;
150             # ABSTRACT: Check whether file has unsaved modification in an editor
151              
152             __END__