Tracking Unix Attributes and Symlinks
In Progress
I currently have a branch of monotone-0.29 (net.venge.monotone.cshore.attr-scan) which adds unix user, permission, and symlink tracking. It will eventually hit mainline. In the meantime, you may want to try the following:
Workaround
The following scripts and monotonerc are designed to make it easy to keep track of owner, group, file permissions mode, and the target of symlinks (and the fact that a given file/directory is really a symlink). Unfortunately the commands that check and update attributes are rather slow because monotone's attr get/set commands are painfully slow.
It should be noted that mtn-dosh is insecure. If a shell metacharacters are used in the temporary filename bad things could happen.
Core
monotonerc
function getdbgval()
return 0
end
function tmpname()
local tmpname = '/tmp/mtn.tmp.'
local getrand
local geterrstr
local randhnd, errstr = io.open('/dev/urandom')
if (randhnd) then
getrand, geterrstr = randhnd:read(8)
if (getrand) then
math.randomseed(string.byte(getrand, 1, 8))
else
print(geterrstr)
math.randomseed(os.time())
end
randhnd:close()
for digitnum = 1,10 do
tmpname = tmpname .. string.char(math.random(string.byte('a'),string.byte('z')))
end
else
print(errstr)
tmpname = tmpname .. 'XXXXXXXXXX'
end
if (getdbgval() > 2) then print("Temp file is: " .. tmpname) end
return tmpname
end
function argstr(...)
local argstr = ""
for i,v in ipairs(arg) do
if (argstr ~= "") then
argstr = argstr .. ' '
end
argstr = argstr .. tostring(v)
end
if (getdbgval() > 3) then print("Argument string is: " .. argstr) end
return argstr
end
function execout(command, ...)
local out = nil
local exec_retval
local tmpfile = tmpname()
local outhnd
local errstr
local out
exec_retval = execute('mtn-dosh', tmpfile, command, unpack(arg))
if (exec_retval == 0) then
outhnd, errstr = io.open(tmpfile)
if (outhnd) then
out, errstr = outhnd:read()
if (out == nil) then
if (getdbgval() > 1) then
print("Error reading " .. tmpfile .. ": " .. errstr)
end
end
outhnd:close()
os.remove(tmpfile)
else
print("Error opening " .. tmpfile .. ": " .. errstr)
os.remove(tmpfile)
end
else
print('Error executing ' .. argstr(command, unpack(arg)) .. ' > ' .. tmpfile .. ': ' .. tostring(exec_retval))
os.remove(tmpfile)
end
if (getdbgval() > 1) then
if (out ~= nil) then
print('execout; got ' .. out)
else
print('no output')
end
end
return out
end
function get_defperms(filetype, is_executable)
local defperms
local perms = execout('umask')
if (perms) then
if ((filetype == "regular file") or (filetype == "regular empty file")) then
if (is_executable) then
defperms = 777 - tonumber(string.sub(perms, 2))
else
defperms = 666 - tonumber(string.sub(perms, 2))
end
elseif (filetype == "directory") then
defperms = 777 - tonumber(string.sub(perms, 2))
else
if (getdbgval() > 2) then
print("Not a regular file or directory, is: " .. filetype)
end
defperms = nil
end
else
defperms = nil
end
if (defperms) then
defpermsstr = string.format("%03d", defperms)
end
if ((getdbgval() > 2) and defpermsstr) then
print("defperms = " .. defpermsstr)
end
return defpermsstr
end
function get_defuser()
defuser = execout('id', '-u')
if ((getdbgval() > 2) and defuser) then
print("defuser = " .. defuser)
end
return defuser
end
function get_defgroup()
defgroup = execout('id', '-g')
if ((getdbgval() > 2) and defgroup) then
print("defgroup = " .. defgroup)
end
return defgroup
end
function has_perms(filename)
local defperms
local perms = execout('stat', '-c', '%a', filename)
local permnum
local retperm = nil
local filetype
if (perms) then
filetype = execout('stat', '-c', '%F', filename)
if (filetype) then
defperms = get_defperms(filetype, is_executable(filename))
if (defperms ~= nil) then
permnum = tonumber(perms)
if (permnum ~= tonumber(defperms)) then
retperm = string.format('%03d', permnum)
end
end
end
end
if ((getdbgval() > 2) and retperm) then
print("perms = " .. retperm)
end
return retperm
end
function has_user(filename)
local user = execout('stat', '-c', '%u', filename)
local defuser = get_defuser()
local retuser = nil
if (user) then
if (defuser) then
if (user ~= defuser) then
retuser = user
end
end
end
if ((getdbgval() > 2) and retuser) then
print("user = " .. retuser)
end
return retuser
end
function is_symlink(filename)
local filetype = execout('stat', '-c', '%F', filename)
local link_target
local retlink = nil
if (filetype == "symbolic link") then
link_target = execout('readlink', filename)
if (link_target) then
if (link_target ~= "" ) then
retlink = link_target
end
end
end
if ((getdbgval() > 2) and retlink) then
print("linktarget = " .. retlink)
end
return retlink
end
function has_group(filename)
local group = execout('stat', '-c', '%g', filename)
local defgroup = get_defgroup()
local retgroup = nil
if (group) then
if (defgroup) then
if (group ~= defgroup) then
retgroup = group
end
end
end
if ((getdbgval() > 2) and retgroup) then
print("group = " .. retgroup)
end
return retgroup
end
attr_init_functions["perms"] = function(filename)
return has_perms(filename)
end
attr_init_functions["user"] = function(filename)
return has_user(filename)
end
attr_init_functions["group"] = function(filename)
return has_group(filename)
end
attr_init_functions["symlink"] = function(filename)
return is_symlink(filename)
end
attr_functions["perms"] = function(filename, value)
execute("/bin/chmod", value, filename)
end
attr_functions["user"] = function(filename, value)
execute("/bin/chown", value, filename)
end
attr_functions["group"] = function(filename, value)
execute("/bin/chgrp", value, filename)
end
attr_functions["symlink"] = function(filename, value)
execute("/bin/mv", filename, filename .. ".old")
if (execute("/bin/ln", "-s", value, filename) == 0) then
os.remove(filename .. ".old")
else
execute("/bin/cp -a", filename .. ".old", filename)
end
end
mtn-dosh
Execute the specified shell command, redirecting output to the file named by the first parameter
It should be noted that mtn-dosh is insecure. If a shell metacharacters are used in the temporary filename bad things could happen.
#!/bin/sh
TMPFILE=$1
shift
$1 $2 $3 $4 $5 $6 $7 $8 $9 > $TMPFILE
NOTE
At this point the following scripts don't work because the mtn attr commands reset the attributes of the files on the filesystem to what's recorded in the monotone workspace before performing the mtn attr set command.
mtn-attr-update
This script does the determines the file's attributes and adds or updates them in monotone. In the interests of performance, only attributes that differ from the expected norm (666-umask for non-exe, 777-umask for exe and dirs, owner:group = current user and group) are stored.
#!/bin/sh
PREFIX=/usr/bin
OLDBIN=$PREFIX/monotone
NEWBIN=$PREFIX/mtn
dirlist=$@
# Figure out default permissions for regular files
fmask=$(expr 666 - $(expr substr $(umask) 2 3))
# Figure out default permissions for executables
emask=$(expr 777 - $(expr substr $(umask) 2 3))
# Figure out default permissions for directories
dmask=$(expr 777 - $(expr substr $(umask) 2 3))
# Default user and group
duser=$(id -u)
dgroup=$(id -g)
echo -n "Default file:exe:dir:user:group = "
echo "$fmask:$emask:$dmask:$duser:$dgroup"
#if [ ! -d "MT" ] && [ ! -d "_MTN" ] ; then
# echo "Error: not in a monotone working directory"
# exit 1
#fi
if [ -x $NEWBIN ]; then
ver="0.26+"
BIN=$NEWBIN
elif [ -x $OLDBIN ]; then
ver="0.25-"
BIN=$OLDBIN
else
echo "Can't find monotone in $PREFIX"
exit 2
fi
checkperms () {
perms=$(stat -c '%a' $1)
if [ "$2" = "normal" ]; then
filetype=$(stat --format='%F' $1)
if [ "$filetype" = "regular file" ] || [ "$filetype" = "regular empty file" ]; then
if [ "$perms" != "$fmask" ]; then
if [ "$perms" = "$emask" ]; then
if [ "$ver" = "0.25-" ]; then
$BIN attr set "$1" executable true
INCLUDEFILE=true
fi
else
$BIN attr set "$1" perms "$perms"
INCLUDEFILE=true
fi
fi
elif [ "$filetype" = "directory" ]; then
if [ "$perms" != "$dmask" ]; then
$BIN attr set "$1" perms "$perms"
INCLUDEFILE=true
fi
fi
elif [ "$perms" != "$2" ]; then
$BIN attr set "$1" perms "$perms"
INCLUDEFILE=true
fi
}
checkuser () {
owner=$(stat -c '%u' $1)
if [ "$owner" != "$2" ]; then
if [ "$owner" = "$duser" ]; then
$BIN attr drop "$1" owner
INCLUDEFILE=true
else
$BIN attr set "$1" owner "$owner"
INCLUDEFILE=true
fi
fi
}
checkgroup () {
group=$(stat -c '%g' $1)
if [ "$group" != "$2" ]; then
if [ "$group" = "$dgroup" ]; then
$BIN attr drop "$1" owner
INCLUDEFILE=true
else
$BIN attr set "$1" group "$group"
INCLUDEFILE=true
fi
fi
}
checksymlink () {
filetype=$(stat --format='%F' $1)
if [ "$2" = "normal" ]; then
if [ "$filetype" = "symbolic link" ]; then
ltarget=$(lname=$(stat -c '%N' $1 | cut -f3 -d\ ) ; echo $(expr substr "$lname" 2 $(expr $(echo $lname | wc | tr -s \ | cut -f4 -d\ ) - 3 ) ) )
$BIN attr set "$1" symlink "$ltarget"
INCLUDEFILE=true
fi
elif [ "$filetype" != "symbolic link" ]; then
$BIN attr drop "$1" symlink
else
ltarget=$(lname=$(stat -c '%N' $1 | cut -f3 -d\ ) ; echo $(expr substr "$lname" 2 $(expr $(echo $lname | wc | tr -s \ | cut -f4 -d\ ) - 3 ) ) )
if [ "$ltarget" != "$2" ]; then
$BIN attr set "$1" symlink "$ltarget"
INCLUDEFILE=true
fi
fi
}
if [ "'$@'" != "''" ]; then
echo "Checking for new or changed non-standard attributes for $@"
else
echo "Checking known files for new or changed non-standard attributes"
fi
filelist=$($BIN list known $@)
IFS=$'\n'
for file in $filelist; do
INCLUDEFILE=false
echo -n "Getting recorded attributes for $file ... "
recattr=$($BIN attr get $file)
if [ "$recattr" = "No attributes for '$file'" ]; then
echo "No attributes recorded."
echo -n "Examining attributes of $file ... "
checkperms $file normal
checkuser $file $duser
checkgroup $file $dgroup
checksymlink $file normal
if [ "$INCLUDEFILE" = "true" ]; then
echo "has changed; recorded."
else
echo "unchanged."
fi
else
echo "has attributes; rechecking."
echo -n "Examining attributes of $file ... "
IFS=$'\n'
for attribute in $recattr ; do
hasperms=0
hasuser=0
hasgroup=0
hassymlink=0
duple=$(echo $attribute | cut -f3 -d\ )
attrname=$(echo $duple | cut -f1 -d'=' )
attrval=$(echo $duple | cut -f2 -d'=' )
case $attrname in
perms)
checkperms $file $attrval
hasperms=1
;;
user)
checkuser $file $attrval
hasuser=1
;;
group)
checkgroup $file $attrval
hasgroup=1
;;
symlink)
checksymlink $file $attrval
hassymlink=1
;;
esac
if [ "$hasperms" = "0" ]; then
checkperms $file normal
fi
if [ "$hasuser" = "0" ]; then
checkuser $file $duser
fi
if [ "$hasgroup" = "0" ]; then
checkgroup $file $dgroup
fi
if [ "$hassymlink" = "0" ]; then
checksymlink $file normal
fi
done
if [ "$INCLUDEFILE" = "true" ]; then
echo "has changed; recorded."
else
echo "unchanged."
fi
fi
done
Convenience Scripts
mtn-commit
Rechecks all monotone managed files in the current workspace to see if their attributes have changed, and if so, records the new state.
#!/bin/sh
PREFIX=/usr/bin
OLDBIN=$PREFIX/monotone
NEWBIN=$PREFIX/mtn
mtn-attr-update $@
$BIN mtn commit $@