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 $@