ktrace(1)를 이용한 자동 plist 생성

포트를 만들거나 업글할 때 가장 귀찮은 과정 중의 하나인 plist를 자동으로 만드는 방법은 [WWW]mtree기반으로 된 것이 있었지만, 뭔가 다른 프리픽스로 한 번 깔아보는 게 대략 귀찮은 관계로.. [FreeBSDMan]ktrace 를 이용한 방법을 한 번 생각해 봤습니다. 흐흐흐.. open 시스템 콜을 모두 추적해서 O_WRONLY나 O_CREAT로 열린 녀석들 목록을 자동으로 plist로 만들어주는 것인데, 대충 만들어 본 소스는..

  1 
  2 
  3 
  4 
  5 
  6 
  7 
  8 
  9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
 63 
 64 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
 79 
 80 
 81 
 82 
 83 
 84 
 85 
 86 
 87 
 88 
 89 
 90 
 91 
 92 
 93 
 94 
 95 
 96 
 97 
 98 
 99 
100 
101 
102 
103 
104 
105 
106 
107 
108 
109 
110 
111 
112 
113 
114 
115 
116 
117 
118 
119 
120 
121 
122 
123 
124 
125 
126 
127 
128 
129 
130 
131 
132 
133 
134 
135 
136 
137 
138 
139 
140 
141 
142 
143 
144 
145 
146 
147 
148 
149 
150 
151 
152 
153 
154 
155 
156 
157 
158 
159 
160 
161 
162 
163 
164 
165 
166 
167 
168 
169 
170 
171 
172 
173 
174 
175 
176 
177 
178 
179 
180 
181 
182 
183 
184 
185 
186 
187 
188 
189 
#!/usr/bin/env python
#
# autoplist.py - Automatic pkg-plist generator based on ktrace
#
# ----------------------------------------------------------------------------
# "THE BEER-WARE LICENSE" (Revision 42, (c) Poul-Henning Kamp):
# Hye-Shik Chang <perky@FreeBSD.org> wrote this file.  As long as you retain
# this notice you can do whatever you want with this stuff. If we meet some
# day, and you think this stuff is worth it, you can buy me a beer in return.
#
# Hye-Shik Chang
# ----------------------------------------------------------------------------
# $FreeBSD$

import os, random
import sys, re
import time

random.whseed()
TMPNAM = '.autoplist.%d.%x' % (os.getpid(), random.randrange(65536))
KTRACECMD = 'ktrace -f %s -id -t cn %%s' % TMPNAM
KDUMPCMD = 'kdump -f %s' % TMPNAM
O_WFLAGS = os.O_WRONLY | os.O_RDWR | os.O_CREAT
PREFIX = '/usr/local'
IGNOREPREFIX = ['/dev/', '/tmp/']
PLIST_SUBS = (
    'PYTHON_INCLUDEDIR', 'PYTHON_LIBDIR', 'PYTHON_SITELIBDIR',
    'SITE_PERL', 'SITE_RUBY', 'DOCSDIR', 'EXAMPLESDIR', 'DATADIR',
)
PLISTREPLACE = []

def errorexit(msg):
    print >> sys.stdout, "autoplist.py:", msg
    sys.exit(-1)

def cleanup_tmpfiles():
    try:
        os.unlink(TMPNAM)
    except:
        pass

def getmakeenv(envname):
    return os.popen('make -V "%s"' % envname).read().strip()

def echo_msg(msg):
    print '===>  ', msg

class InstallState:

    procs = {}
    lastforker = 0

    def __init__(self, pid, procname):
        self.pid = pid
        self.procname = procname
        if not self.lastforker:
            self.curdir = os.getcwd()
        else:
            self.curdir = self.procs[self.lastforker].curdir
        self.procs[pid] = self
        self.files, self.dirs = {}, {}
        self.trackatom = {}

    def feed(self, args):
        if self.trackatom:
            if args[2] == 'NAMI':
                self.trackatom['nami'].append(eval(args[3]))
            elif args[2] == 'RET':
                rtok = args[3].split()
                if rtok[1].isdigit() or rtok[1].startswith('0x'):
                    self.trackatom['exit'] = eval(rtok[1])
                else:
                    self.trackatom['exit'] = rtok[1]
                if len(rtok) >= 3:
                    self.trackatom['errno'] = eval(rtok[3])
                self.newsyscall(**self.trackatom)
                self.trackatom = {}
        elif args[2] == 'CALL':
            syscall, ignore, args = re_syscallargs.findall(args[3])[0]
            self.trackatom['syscall'] = syscall
            self.trackatom['args'] = args and args.split(',') or []
            self.trackatom['nami'] = []
            if syscall in ('fork', 'vfork', 'rfork'):
                InstallState.lastforker = self.pid

    def getabspath(self, path):
        if path.startswith('/'):
            return path
        else:
            return os.path.realpath(os.sep.join([self.curdir, path]))

    def newsyscall(self, syscall, args, nami, exit, errno=0):
        if syscall == 'open':
            if eval(args[1]) & O_WFLAGS:
                self.files[self.getabspath(nami[0])] = None
        elif syscall == 'mkdir':
            self.dirs[self.getabspath(nami[0])] = None
        elif syscall == 'chdir' and exit == 0:
            self.curdir = self.getabspath(nami[0])
        elif syscall in ('execve',) and exit == 0:
            self.procname = nami[0]

def filterfilelist(files):
    filelist = []
    warnlist = []
    prefix = PREFIX + os.sep
    for f in files:
        for pfx in IGNOREPREFIX:
            if f.startswith(pfx):
                continue
        if f.startswith(prefix):
            filelist.append(f[len(prefix):])
            for var, repl in PLISTREPLACE:
                if filelist[-1].startswith(repl):
                    filelist[-1] = filelist[-1].replace(repl, '%%'+var+'%%')
        else:
            warnlist.append(f)
    return filelist, warnlist

re_syscallargs = re.compile('([A-Za-z0-9_-]+)(\(([^)]*)\))?')
def getwfiles():
    entry = {}
    istate = InstallState.procs
    echo_msg('Analyzing installation syscall logs')
    for l in os.popen(KDUMPCMD):
        ltok = l[:-1].split(None, 3)
        pid = int(ltok[0])
        if not istate.has_key(pid):
            proc = istate[pid] = InstallState(pid, ltok[1])
        else:
            proc = istate[pid]
        proc.feed(ltok)

    echo_msg('Generating auto plist')
    fo = open("pkg-plist.autogen", "w")
    print >>fo, "@comment Generated by autoplist.py", time.asctime()

    files = {}
    dirs = {}
    for pid, data in istate.iteritems():
        files.update(data.files)
        dirs.update(data.dirs)

    files = files.keys()
    files.sort()
    files, warnfiles = filterfilelist(files)
    for file in files:
        print >>fo, file

    dirs = dirs.keys()
    dirs.sort()
    dirs.reverse()
    dirs, warndirs = filterfilelist(dirs)
    for dir in dirs:
        print >>fo, "@dirrm", dir

    return istate

def main():
    try:
        err = os.system('make build')
        if err != 0:
            errorexit("`make' exited with error code %d" % err)
        err = os.system(KTRACECMD % 'make install')
        if err != 0:
            errorexit("`make' exited with error code %d" % err)
        getwfiles()
    finally:
        cleanup_tmpfiles()

def buildenvironment():
    global IGNOREPREFIX, PREFIX, PLISTREPLACE

    IGNOREPREFIX.append(os.path.abspath(getmakeenv('WRKDIRPREFIX')))
    IGNOREPREFIX.append(os.path.abspath(getmakeenv('DISTDIR')))
    IGNOREPREFIX.append(os.path.abspath(getmakeenv('PKG_DBDIR')))
    PREFIX = getmakeenv('PREFIX')

    for var, raw, stripped in re.findall('([A-Za-z_0-9]+)=("([^"]*)"|[^"]\S*)',
                                getmakeenv('PLIST_SUB')):
        if var in PLIST_SUBS:
            PLISTREPLACE.append((var, stripped or raw))
    PLISTREPLACE.sort(lambda x,y: -cmp(x[1], y[1]))

if __name__ == '__main__':
    buildenvironment()
    main()

# ex: ts=9 sts=4 sw=4 et

흐흐.. 그런데, 대충 만들고 나서 보니, 속도가 너무 느리고 (좀만 대형 포트로 가면 엄청난 양의 kdump가 나와버려서 그것 파싱하는데 십분이 넘게 걸립니다 –;) mtree기반의 녀석에게 특별한 장점이 없다는 결론이.. 흐흐흐… (그리고, 위 구현에서는 디렉토리를 인스톨 도중에 mv 해 버린다던지 하는 것에 대해서 무방비 –;)

그래서, 이 녀석을 좀 변모시켜서 plistlint 정도의 이름으로 인스톨하면서 plist에 없는 프로그램 건드리지는 않나, 빼먹은 파일 없나, /tmp에 이상한 파일 남기지 않나 등등을 검사하는 걸로 만들어 볼 까 합니당. 크크

12 thoughts on “ktrace(1)를 이용한 자동 plist 생성”

Comments are closed.