blob: a925baf21198a6ee29be09c9cd3ba0479dd65b35 [file] [log] [blame]
/*
* dpkg - main program for package management
* configure.c - configure packages
*
* Copyright © 1995 Ian Jackson <ian@chiark.greenend.org.uk>
* Copyright © 1999, 2002 Wichert Akkerman <wichert@deephackmode.org>
*
* This is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <config.h>
#include <compat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/termios.h>
#include <assert.h>
#include <errno.h>
#include <ctype.h>
#include <string.h>
#include <time.h>
#include <fcntl.h>
#include <dirent.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <dpkg/macros.h>
#include <dpkg/i18n.h>
#include <dpkg/dpkg.h>
#include <dpkg/dpkg-db.h>
#include <dpkg/string.h>
#include <dpkg/buffer.h>
#include <dpkg/file.h>
#include <dpkg/path.h>
#include <dpkg/subproc.h>
#include <dpkg/command.h>
#include <dpkg/triglib.h>
#include "filesdb.h"
#include "main.h"
static int conffoptcells[2][2] = {
/* Distro !edited. */ /* Distro edited. */
{ cfo_keep, cfo_install }, /* User !edited. */
{ cfo_keep, cfo_prompt_keep }, /* User edited. */
};
static void md5hash(struct pkginfo *pkg, char *hashbuf, const char *fn);
static void showdiff(const char *old, const char *new);
static enum conffopt promptconfaction(struct pkginfo *pkg, const char *cfgfile,
const char *realold, const char *realnew,
int useredited, int distedited,
enum conffopt what);
static void
deferred_configure_conffile(struct pkginfo *pkg, struct conffile *conff)
{
struct filenamenode *usenode;
static const char EMPTY_HASH[] = "-";
char currenthash[MD5HASHLEN + 1], newdisthash[MD5HASHLEN + 1];
int useredited, distedited;
enum conffopt what;
struct stat stab;
struct varbuf cdr = VARBUF_INIT, cdr2 = VARBUF_INIT;
char *cdr2rest;
int r;
usenode = namenodetouse(findnamenode(conff->name, fnn_nocopy), pkg);
r = conffderef(pkg, &cdr, usenode->name);
if (r == -1) {
conff->hash = EMPTY_HASH;
return;
}
md5hash(pkg, currenthash, cdr.buf);
varbuf_reset(&cdr2);
varbuf_add_str(&cdr2, cdr.buf);
varbuf_end_str(&cdr2);
/* XXX: Make sure there's enough room for extensions. */
varbuf_grow(&cdr2, 50);
cdr2rest = cdr2.buf + strlen(cdr.buf);
/* From now on we can just strcpy(cdr2rest, extension); */
strcpy(cdr2rest, DPKGNEWEXT);
/* If the .dpkg-new file is no longer there, ignore this one. */
if (lstat(cdr2.buf, &stab)) {
if (errno == ENOENT)
return;
ohshite(_("unable to stat new distributed conffile '%.250s'"),
cdr2.buf);
}
md5hash(pkg, newdisthash, cdr2.buf);
/* Copy the permissions from the installed version to the new
* distributed version. */
if (!stat(cdr.buf, &stab))
file_copy_perms(cdr.buf, cdr2.buf);
else if (errno != ENOENT)
ohshite(_("unable to stat current installed conffile `%.250s'"),
cdr.buf);
/* Select what to do. */
if (!strcmp(currenthash, newdisthash)) {
/* They're both the same so there's no point asking silly
* questions. */
useredited = -1;
distedited = -1;
what = cfo_identical;
} else if (!strcmp(currenthash, NONEXISTENTFLAG) && fc_conff_miss) {
fprintf(stderr,
_("\n"
"Configuration file `%s', does not exist on system.\n"
"Installing new config file as you requested.\n"),
usenode->name);
what = cfo_newconff;
useredited = -1;
distedited = -1;
} else if (!strcmp(conff->hash, NEWCONFFILEFLAG)) {
if (!strcmp(currenthash, NONEXISTENTFLAG)) {
what = cfo_newconff;
useredited = -1;
distedited = -1;
} else {
useredited = 1;
distedited = 1;
what = conffoptcells[useredited][distedited] |
cfof_isnew;
}
} else {
useredited = strcmp(conff->hash, currenthash) != 0;
distedited = strcmp(conff->hash, newdisthash) != 0;
if (fc_conff_ask && useredited)
what = cfo_prompt_keep;
else
what = conffoptcells[useredited][distedited];
if (!strcmp(currenthash, NONEXISTENTFLAG))
what |= cfof_userrmd;
}
debug(dbg_conff,
"deferred_configure '%s' (= '%s') useredited=%d distedited=%d what=%o",
usenode->name, cdr.buf, useredited, distedited, what);
what = promptconfaction(pkg, usenode->name, cdr.buf, cdr2.buf,
useredited, distedited, what);
switch (what & ~(cfof_isnew | cfof_userrmd)) {
case cfo_keep | cfof_backup:
strcpy(cdr2rest, DPKGOLDEXT);
if (unlink(cdr2.buf) && errno != ENOENT)
warning(_("%s: failed to remove old backup '%.250s': %s"),
pkg->name, cdr2.buf, strerror(errno));
varbuf_add_str(&cdr, DPKGDISTEXT);
varbuf_end_str(&cdr);
strcpy(cdr2rest, DPKGNEWEXT);
trig_file_activate(usenode, pkg);
if (rename(cdr2.buf, cdr.buf))
warning(_("%s: failed to rename '%.250s' to '%.250s': %s"),
pkg->name, cdr2.buf, cdr.buf, strerror(errno));
break;
case cfo_keep:
strcpy(cdr2rest, DPKGNEWEXT);
if (unlink(cdr2.buf))
warning(_("%s: failed to remove '%.250s': %s"),
pkg->name, cdr2.buf, strerror(errno));
break;
case cfo_install | cfof_backup:
strcpy(cdr2rest, DPKGDISTEXT);
if (unlink(cdr2.buf) && errno != ENOENT)
warning(_("%s: failed to remove old distributed version '%.250s': %s"),
pkg->name, cdr2.buf, strerror(errno));
strcpy(cdr2rest, DPKGOLDEXT);
if (unlink(cdr2.buf) && errno != ENOENT)
warning(_("%s: failed to remove '%.250s' (before overwrite): %s"),
pkg->name, cdr2.buf, strerror(errno));
if (!(what & cfof_userrmd))
if (link(cdr.buf, cdr2.buf))
warning(_("%s: failed to link '%.250s' to '%.250s': %s"),
pkg->name, cdr.buf, cdr2.buf, strerror(errno));
/* Fall through. */
case cfo_install:
printf(_("Installing new version of config file %s ...\n"),
usenode->name);
case cfo_newconff:
strcpy(cdr2rest, DPKGNEWEXT);
trig_file_activate(usenode, pkg);
if (rename(cdr2.buf, cdr.buf))
ohshite(_("unable to install `%.250s' as `%.250s'"),
cdr2.buf, cdr.buf);
break;
default:
internerr("unknown conffopt '%d'", what);
}
conff->hash = nfstrsave(newdisthash);
modstatdb_note(pkg);
varbuf_destroy(&cdr);
varbuf_destroy(&cdr2);
}
/**
* Process the deferred configure package.
*
* The algorithm for deciding what to configure first is as follows:
* Loop through all packages doing a ‘try 1’ until we've been round
* and nothing has been done, then do ‘try 2’ and ‘try 3’ likewise.
* The incrementing of ‘dependtry’ is done by process_queue().
*
* Try 1:
* Are all dependencies of this package done? If so, do it.
* Are any of the dependencies missing or the wrong version?
* If so, abort (unless --force-depends, in which case defer).
* Will we need to configure a package we weren't given as an
* argument? If so, abort ─ except if --force-configure-any,
* in which case we add the package to the argument list.
* If none of the above, defer the package.
*
* Try 2:
* Find a cycle and break it (see above).
* Do as for try 1.
*
* Try 3 (only if --force-depends-version):
* Same as for try 2, but don't mind version number in dependencies.
*
* Try 4 (only if --force-depends):
* Do anyway.
*
* @param pkg The package to act on.
*/
void
deferred_configure(struct pkginfo *pkg)
{
struct varbuf aemsgs = VARBUF_INIT;
struct conffile *conff;
int ok;
if (pkg->status == stat_notinstalled)
ohshit(_("no package named `%s' is installed, cannot configure"),
pkg->name);
if (pkg->status == stat_installed)
ohshit(_("package %.250s is already installed and configured"),
pkg->name);
if (pkg->status != stat_unpacked && pkg->status != stat_halfconfigured)
ohshit(_("package %.250s is not ready for configuration\n"
" cannot configure (current status `%.250s')"),
pkg->name, statusinfos[pkg->status].name);
if (dependtry > 1)
if (findbreakcycle(pkg))
sincenothing = 0;
ok = dependencies_ok(pkg, NULL, &aemsgs);
if (ok == 1) {
varbuf_destroy(&aemsgs);
pkg->clientdata->istobe = itb_installnew;
add_to_queue(pkg);
return;
}
trigproc_reset_cycle();
/*
* At this point removal from the queue is confirmed. This
* represents irreversible progress wrt trigger cycles. Only
* packages in stat_unpacked are automatically added to the
* configuration queue, and during configuration and trigger
* processing new packages can't enter into unpacked.
*/
ok = breakses_ok(pkg, &aemsgs) ? ok : 0;
if (ok == 0) {
sincenothing = 0;
varbuf_end_str(&aemsgs);
fprintf(stderr,
_("dpkg: dependency problems prevent configuration of %s:\n%s"),
pkg->name, aemsgs.buf);
varbuf_destroy(&aemsgs);
ohshit(_("dependency problems - leaving unconfigured"));
} else if (aemsgs.used) {
varbuf_end_str(&aemsgs);
fprintf(stderr,
_("dpkg: %s: dependency problems, but configuring anyway as you requested:\n%s"),
pkg->name, aemsgs.buf);
}
varbuf_destroy(&aemsgs);
sincenothing = 0;
if (pkg->eflag & eflag_reinstreq)
forcibleerr(fc_removereinstreq,
_("Package is in a very bad inconsistent state - you should\n"
" reinstall it before attempting configuration."));
printf(_("Setting up %s (%s) ...\n"), pkg->name,
versiondescribe(&pkg->installed.version, vdew_nonambig));
log_action("configure", pkg);
trig_activate_packageprocessing(pkg);
if (f_noact) {
pkg->status = stat_installed;
pkg->clientdata->istobe = itb_normal;
return;
}
if (pkg->status == stat_unpacked) {
debug(dbg_general, "deferred_configure updating conffiles");
/* This will not do at all the right thing with overridden
* conffiles or conffiles that are the ‘target’ of an override;
* all the references here would be to the ‘contested’
* filename, and in any case there'd only be one hash for both
* ‘versions’ of the conffile.
*
* Overriding conffiles is a silly thing to do anyway :-). */
modstatdb_note(pkg);
/* On entry, the ‘new’ version of each conffile has been
* unpacked as ‘*.dpkg-new’, and the ‘installed’ version is
* as-yet untouched in ‘*’. The hash of the ‘old distributed’
* version is in the conffiles data for the package. If
* ‘*.dpkg-new’ no longer exists we assume that we've
* already processed this one. */
for (conff = pkg->installed.conffiles; conff; conff = conff->next)
deferred_configure_conffile(pkg, conff);
pkg->status = stat_halfconfigured;
}
assert(pkg->status == stat_halfconfigured);
modstatdb_note(pkg);
maintainer_script_postinst(pkg, "configure",
informativeversion(&pkg->configversion) ?
versiondescribe(&pkg->configversion,
vdew_nonambig) : "",
NULL);
pkg->eflag = eflag_ok;
pkg->trigpend_head = NULL;
post_postinst_tasks(pkg, stat_installed);
}
/**
* Dereference a file by following all possibly used symlinks.
*
* @param[in] pkg The package to act on.
* @param[out] result The dereference conffile path.
* @param[in] in The conffile path to dereference.
*
* @return An error code for the operation.
* @retval 0 Everything went ok.
* @retval -1 Otherwise.
*/
int
conffderef(struct pkginfo *pkg, struct varbuf *result, const char *in)
{
static struct varbuf target = VARBUF_INIT;
struct stat stab;
ssize_t r;
int loopprotect;
varbuf_reset(result);
varbuf_add_str(result, instdir);
if (*in != '/')
varbuf_add_char(result, '/');
varbuf_add_str(result, in);
varbuf_end_str(result);
loopprotect = 0;
for (;;) {
debug(dbg_conffdetail, "conffderef in='%s' current working='%s'",
in, result->buf);
if (lstat(result->buf, &stab)) {
if (errno != ENOENT)
warning(_("%s: unable to stat config file '%s'\n"
" (= '%s'): %s"),
pkg->name, in, result->buf, strerror(errno));
debug(dbg_conffdetail, "conffderef nonexistent");
return 0;
} else if (S_ISREG(stab.st_mode)) {
debug(dbg_conff, "conffderef in='%s' result='%s'",
in, result->buf);
return 0;
} else if (S_ISLNK(stab.st_mode)) {
debug(dbg_conffdetail, "conffderef symlink loopprotect=%d",
loopprotect);
if (loopprotect++ >= 25) {
warning(_("%s: config file '%s' is a circular link\n"
" (= '%s')"), pkg->name, in, result->buf);
return -1;
}
varbuf_reset(&target);
varbuf_grow(&target, stab.st_size + 1);
r = readlink(result->buf, target.buf, target.size);
if (r < 0) {
warning(_("%s: unable to readlink conffile '%s'\n"
" (= '%s'): %s"),
pkg->name, in, result->buf, strerror(errno));
return -1;
} else if (r != stab.st_size) {
warning(_("symbolic link '%.250s' size has "
"changed from %jd to %zd"),
result->buf, stab.st_size, r);
return -1;
}
varbuf_trunc(&target, r);
varbuf_end_str(&target);
debug(dbg_conffdetail,
"conffderef readlink gave %zd, '%s'",
r, target.buf);
if (target.buf[0] == '/') {
varbuf_reset(result);
varbuf_add_str(result, instdir);
debug(dbg_conffdetail,
"conffderef readlink absolute");
} else {
for (r = result->used - 1; r > 0 && result->buf[r] != '/'; r--)
;
if (r < 0) {
warning(_("%s: conffile '%.250s' resolves to degenerate filename\n"
" ('%s' is a symlink to '%s')"),
pkg->name, in, result->buf,
target.buf);
return -1;
}
if (result->buf[r] == '/')
r++;
varbuf_trunc(result, r);
debug(dbg_conffdetail,
"conffderef readlink relative to '%.*s'",
(int)result->used, result->buf);
}
varbuf_add_buf(result, target.buf, target.used);
varbuf_end_str(result);
} else {
warning(_("%s: conffile '%.250s' is not a plain file or symlink (= '%s')"),
pkg->name, in, result->buf);
return -1;
}
}
}
/**
* Generate a file contents MD5 hash.
*
* The caller is responsible for providing a buffer for the hash result
* at least MD5HASHLEN + 1 characters long.
*
* @param[in] pkg The package to act on.
* @param[out] hashbuf The buffer to store the generated hash.
* @param[in] fn The filename.
*/
static void
md5hash(struct pkginfo *pkg, char *hashbuf, const char *fn)
{
static int fd;
fd = open(fn, O_RDONLY);
if (fd >= 0) {
push_cleanup(cu_closefd, ehflag_bombout, NULL, 0, 1, &fd);
fd_md5(fd, hashbuf, -1, _("md5hash"));
pop_cleanup(ehflag_normaltidy); /* fd = open(cdr.buf) */
close(fd);
} else if (errno == ENOENT) {
strcpy(hashbuf, NONEXISTENTFLAG);
} else {
warning(_("%s: unable to open conffile %s for hash: %s"),
pkg->name, fn, strerror(errno));
strcpy(hashbuf, "-");
}
}
/**
* Show a diff between two files.
*
* @param old The path to the old file.
* @param new The path to the new file.
*/
static void
showdiff(const char *old, const char *new)
{
pid_t pid;
pid = subproc_fork();
if (!pid) {
/* Child process. */
const char *pager;
char cmdbuf[1024];
pager = getenv("PAGER");
if (!pager || !*pager)
pager = DEFAULTPAGER;
sprintf(cmdbuf, DIFF " -Nu %.250s %.250s | %.250s",
str_quote_meta(old), str_quote_meta(new), pager);
command_shell(cmdbuf, _("conffile difference visualizer"));
}
/* Parent process. */
subproc_wait(pid, _("conffile difference visualizer"));
}
/**
* Spawn a new shell.
*
* Create a subprocess and execute a shell to allow the user to manually
* solve the conffile conflict.
*
* @param confold The path to the old conffile.
* @param confnew The path to the new conffile.
*/
static void
spawn_shell(const char *confold, const char *confnew)
{
pid_t pid;
fputs(_("Type `exit' when you're done.\n"), stderr);
pid = subproc_fork();
if (!pid) {
/* Set useful variables for the user. */
setenv("DPKG_SHELL_REASON", "conffile-prompt", 1);
setenv("DPKG_CONFFILE_OLD", confold, 1);
setenv("DPKG_CONFFILE_NEW", confnew, 1);
command_shell(NULL, _("conffile shell"));
}
/* Parent process. */
subproc_wait(pid, _("conffile shell"));
}
/**
* Prompt the user for how to resolve a conffile conflict.
*
* When encountering a conffile conflict during configuration, the user will
* normally be presented with a textual menu of possible actions. This
* behavior is modified via various --force flags and perhaps on whether
* or not a terminal is available to do the prompting.
*
* @param pkg The package owning the conffile.
* @param cfgfile The path to the old conffile.
* @param realold The path to the old conffile, dereferenced in case of a
* symlink, otherwise equal to cfgfile.
* @param realnew The path to the new conffile, dereferenced in case of a
* symlink).
* @param useredited A flag to indicate whether the file has been edited
* locally. Set to nonzero to indicate that the file has been modified.
* @param distedited A flag to indicate whether the file has been updated
* between package versions. Set to nonzero to indicate that the file
* has been updated.
* @param what Hints on what action should be taken by defualt.
*
* @return The action which should be taken based on user input and/or the
* default actions as configured by cmdline/configuration options.
*/
static enum conffopt
promptconfaction(struct pkginfo *pkg, const char *cfgfile,
const char *realold, const char *realnew,
int useredited, int distedited, enum conffopt what)
{
const char *s;
int c, cc;
if (!(what & cfof_prompt))
return what;
statusfd_send("status: %s : %s : '%s' '%s' %i %i ",
cfgfile, "conffile-prompt",
realold, realnew, useredited, distedited);
do {
/* Flush the terminal's input in case the user involuntarily
* typed some characters. */
tcflush(STDIN_FILENO, TCIFLUSH);
fprintf(stderr, _("\nConfiguration file `%s'"), cfgfile);
if (strcmp(cfgfile, realold))
fprintf(stderr, _(" (actually `%s')"), realold);
if (what & cfof_isnew) {
fprintf(stderr,
_("\n"
" ==> File on system created by you or by a script.\n"
" ==> File also in package provided by package maintainer.\n"));
} else {
fprintf(stderr, !useredited ?
_("\n Not modified since installation.\n") :
!(what & cfof_userrmd) ?
_("\n ==> Modified (by you or by a script) since installation.\n") :
_("\n ==> Deleted (by you or by a script) since installation.\n"));
fprintf(stderr, distedited ?
_(" ==> Package distributor has shipped an updated version.\n") :
_(" Version in package is the same as at last installation.\n"));
}
/* No --force-confdef but a forcible situtation. */
/* TODO: check if this condition can not be simplified to
* just !fc_conff_def */
if (!(fc_conff_def && (what & (cfof_install | cfof_keep)))) {
if (fc_conff_new) {
fprintf(stderr, _(" ==> Using new file as you requested.\n"));
cc = 'y';
break;
} else if (fc_conff_old) {
fprintf(stderr, _(" ==> Using current old file as you requested.\n"));
cc = 'n';
break;
}
}
/* Force the default action (if there is one. */
if (fc_conff_def) {
if (what & cfof_keep) {
fprintf(stderr, _(" ==> Keeping old config file as default.\n"));
cc = 'n';
break;
} else if (what & cfof_install) {
fprintf(stderr, _(" ==> Using new config file as default.\n"));
cc = 'y';
break;
}
}
fprintf(stderr,
_(" What would you like to do about it ? Your options are:\n"
" Y or I : install the package maintainer's version\n"
" N or O : keep your currently-installed version\n"
" D : show the differences between the versions\n"
" Z : start a shell to examine the situation\n"));
if (what & cfof_keep)
fprintf(stderr, _(" The default action is to keep your current version.\n"));
else if (what & cfof_install)
fprintf(stderr, _(" The default action is to install the new version.\n"));
s = path_basename(cfgfile);
fprintf(stderr, "*** %s (Y/I/N/O/D/Z) %s ? ",
s,
(what & cfof_keep) ? _("[default=N]") :
(what & cfof_install) ? _("[default=Y]") :
_("[no default]"));
if (ferror(stderr))
ohshite(_("error writing to stderr, discovered before conffile prompt"));
cc = 0;
while ((c = getchar()) != EOF && c != '\n')
if (!isspace(c) && !cc)
cc = tolower(c);
if (c == EOF) {
if (ferror(stdin))
ohshite(_("read error on stdin at conffile prompt"));
ohshit(_("EOF on stdin at conffile prompt"));
}
if (!cc) {
if (what & cfof_keep) {
cc = 'n';
break;
} else if (what & cfof_install) {
cc = 'y';
break;
}
}
/* FIXME: Say something if silently not install. */
if (cc == 'd')
showdiff(realold, realnew);
if (cc == 'z')
spawn_shell(realold, realnew);
} while (!strchr("yino", cc));
log_message("conffile %s %s", cfgfile,
(cc == 'i' || cc == 'y') ? "install" : "keep");
what &= cfof_userrmd;
switch (cc) {
case 'i':
case 'y':
what |= cfof_install | cfof_backup;
break;
case 'n':
case 'o':
what |= cfof_keep | cfof_backup;
break;
default:
internerr("unknown response '%d'", cc);
}
return what;
}