git-annex/Assistant/Upgrade.hs

378 lines
12 KiB
Haskell
Raw Normal View History

{- git-annex assistant upgrading
-
- Copyright 2013 Joey Hess <id@joeyh.name>
-
- Licensed under the GNU AGPL version 3 or higher.
-}
2013-11-24 03:45:49 +00:00
{-# LANGUAGE CPP #-}
module Assistant.Upgrade where
import Assistant.Common
import Assistant.Restart
import qualified Annex
import Assistant.Alert
import Assistant.DaemonStatus
import Utility.Env
import Utility.Env.Set
import Types.Distribution
import Types.Transfer
2013-11-24 01:58:39 +00:00
import Logs.Web
2013-11-24 03:45:49 +00:00
import Logs.Presence
import Logs.Location
2013-11-24 01:58:39 +00:00
import Annex.Content
2014-12-17 17:57:52 +00:00
import Annex.UUID
2013-11-24 01:58:39 +00:00
import qualified Backend
import qualified Types.Backend
import Assistant.TransferQueue
import Assistant.TransferSlots
2013-11-24 03:45:49 +00:00
import Remote (remoteFromUUID)
import Annex.Path
2013-11-24 03:45:49 +00:00
import Config.Files
import Utility.ThreadScheduler
import Utility.Tmp.Dir
2013-11-24 16:49:03 +00:00
import Utility.UserInfo
import Utility.Gpg
2014-07-03 23:49:26 +00:00
import Utility.FileMode
import Utility.Metered
2013-11-24 03:45:49 +00:00
import qualified Utility.Lsof as Lsof
import qualified BuildInfo
import qualified Utility.Url as Url
import qualified Annex.Url as Url hiding (download)
import Utility.Tuple
import qualified Utility.RawFilePath as R
import Data.Either
import qualified Data.Map as M
{- Upgrade without interaction in the webapp. -}
unattendedUpgrade :: Assistant ()
unattendedUpgrade = do
prepUpgrade
url <- runRestart
postUpgrade url
prepUpgrade :: Assistant ()
prepUpgrade = do
void $ addAlert upgradingAlert
liftIO $ setEnv upgradedEnv "1" True
prepRestart
postUpgrade :: URLString -> Assistant ()
postUpgrade = postRestart
autoUpgradeEnabled :: Assistant Bool
autoUpgradeEnabled = liftAnnex $ (==) AutoUpgrade . annexAutoUpgrade <$> Annex.getGitConfig
checkSuccessfulUpgrade :: IO Bool
checkSuccessfulUpgrade = isJust <$> getEnv upgradedEnv
upgradedEnv :: String
upgradedEnv = "GIT_ANNEX_UPGRADED"
2013-11-24 01:58:39 +00:00
{- Start downloading the distribution key from the web.
2013-11-24 03:45:49 +00:00
- Install a hook that will be run once the download is complete,
- and finishes the upgrade.
-
- Creates the destination directory where the upgrade will be installed
- early, in order to check if another upgrade has happened (or is
- happening). On failure, the directory is removed.
-}
startDistributionDownload :: GitAnnexDistribution -> Assistant ()
startDistributionDownload d = go =<< liftIO . newVersionLocation d =<< liftIO oldVersionLocation
where
go Nothing = debug ["Skipping redundant upgrade"]
go (Just dest) = do
liftAnnex $ setUrlPresent k u
hook <- asIO1 $ distributionDownloadComplete d dest cleanup
modifyDaemonStatus_ $ \s -> s
{ transferHook = M.insert k hook (transferHook s) }
maybe noop (queueTransfer "upgrade" Next (AssociatedFile (Just (toRawFilePath f))) t)
=<< liftAnnex (remoteFromUUID webUUID)
startTransfer t
k = mkKey $ const $ distributionKey d
u = distributionUrl d
f = takeFileName u ++ " (for upgrade)"
t = Transfer
{ transferDirection = Download
, transferUUID = webUUID
, transferKeyData = fromKey id k
}
2013-11-24 03:45:49 +00:00
cleanup = liftAnnex $ do
lockContentForRemoval k noop removeAnnex
setUrlMissing k u
2013-11-24 03:45:49 +00:00
logStatus k InfoMissing
2013-11-24 03:45:49 +00:00
{- Called once the download is done.
- Passed an action that can be used to clean up the downloaded file.
-
- Verifies the content of the downloaded key.
2013-11-24 03:45:49 +00:00
-}
distributionDownloadComplete :: GitAnnexDistribution -> FilePath -> Assistant () -> Transfer -> Assistant ()
distributionDownloadComplete d dest cleanup t
| transferDirection t == Download = do
debug ["finished downloading git-annex distribution"]
maybe (failedupgrade "bad download") go
=<< liftAnnex (withObjectLoc k fsckit)
2013-11-24 16:49:03 +00:00
| otherwise = cleanup
2013-11-24 01:58:39 +00:00
where
k = mkKey $ const $ distributionKey d
fsckit f = Backend.maybeLookupBackendVariety (fromKey keyVariety k) >>= \case
Nothing -> return $ Just (fromRawFilePath f)
Just b -> case Types.Backend.verifyKeyContent b of
Nothing -> return $ Just (fromRawFilePath f)
Just verifier -> ifM (verifier k f)
( return $ Just (fromRawFilePath f)
2013-11-24 03:45:49 +00:00
, return Nothing
2013-11-24 01:58:39 +00:00
)
2013-11-24 18:04:03 +00:00
go f = do
ua <- asIO $ upgradeToDistribution dest cleanup f
2013-11-24 18:04:03 +00:00
fa <- asIO1 failedupgrade
liftIO $ ua `catchNonAsync` (fa . show)
failedupgrade msg = do
void $ addAlert $ upgradeFailedAlert msg
2013-11-24 18:04:03 +00:00
cleanup
liftIO $ void $ tryIO $ removeDirectoryRecursive dest
2013-11-24 01:58:39 +00:00
2013-11-24 03:45:49 +00:00
{- The upgrade method varies by OS.
-
- In general, find where the distribution was installed before,
- and unpack the new distribution next to it (in a versioned directory).
- Then update the programFile to point to the new version.
-}
upgradeToDistribution :: FilePath -> Assistant () -> FilePath -> Assistant ()
upgradeToDistribution newdir cleanup distributionfile = do
2013-11-24 19:53:15 +00:00
liftIO $ createDirectoryIfMissing True newdir
(program, deleteold) <- unpack
changeprogram program
2013-11-24 03:45:49 +00:00
cleanup
prepUpgrade
url <- runRestart
{- At this point, the new assistant is fully running, so
- it's safe to delete the old version. -}
2013-11-24 16:49:03 +00:00
liftIO $ void $ tryIO deleteold
2013-11-24 03:45:49 +00:00
postUpgrade url
where
changeprogram program = liftIO $ do
unlessM (boolSystem program [Param "version"]) $
giveup "New git-annex program failed to run! Not using."
2013-11-24 03:45:49 +00:00
pf <- programFile
liftIO $ writeFile pf program
#ifdef darwin_HOST_OS
{- OS X uses a dmg, so mount it, and copy the contents into place. -}
2013-11-24 19:53:15 +00:00
unpack = liftIO $ do
olddir <- oldVersionLocation
2020-11-12 15:36:14 +00:00
withTmpDirIn (fromRawFilePath (parentDir (toRawFilePath newdir))) "git-annex.upgrade" $ \tmpdir -> do
2013-11-24 19:53:15 +00:00
void $ boolSystem "hdiutil"
2013-11-25 17:10:45 +00:00
[ Param "attach", File distributionfile
2013-11-25 17:53:28 +00:00
, Param "-mountpoint", File tmpdir
2013-11-24 19:53:15 +00:00
]
2013-11-25 17:15:27 +00:00
void $ boolSystem "cp"
[ Param "-R"
2013-11-25 18:37:44 +00:00
, File $ tmpdir </> installBase </> "Contents"
, File $ newdir
2013-11-25 17:15:27 +00:00
]
2013-11-24 19:53:15 +00:00
void $ boolSystem "hdiutil"
[ Param "eject"
2013-11-25 18:01:41 +00:00
, File tmpdir
2013-11-24 19:53:15 +00:00
]
2013-11-25 18:46:33 +00:00
sanitycheck newdir
2013-11-24 19:53:15 +00:00
let deleteold = do
deleteFromManifest $ olddir </> "Contents" </> "MacOS"
makeorigsymlink olddir
2013-11-25 18:52:04 +00:00
return (newdir </> "Contents" </> "MacOS" </> "git-annex", deleteold)
#else
{- Linux uses a tarball (so could other POSIX systems), so
- untar it (into a temp directory) and move the directory
- into place. -}
unpack = liftIO $ do
olddir <- oldVersionLocation
withTmpDirIn (fromRawFilePath $ parentDir $ toRawFilePath newdir) "git-annex.upgrade" $ \tmpdir -> do
let tarball = tmpdir </> "tar"
-- Cannot rely on filename extension, and this also
-- avoids problems if tar doesn't support transparent
-- decompression.
void $ boolSystem "sh"
[ Param "-c"
, Param $ "zcat < " ++ shellEscape distributionfile ++
" > " ++ shellEscape tarball
]
tarok <- boolSystem "tar"
[ Param "xf"
, Param tarball
, Param "--directory", File tmpdir
]
unless tarok $
giveup $ "failed to untar " ++ distributionfile
2013-11-25 18:46:33 +00:00
sanitycheck $ tmpdir </> installBase
installby R.rename newdir (tmpdir </> installBase)
2013-11-24 16:49:03 +00:00
let deleteold = do
deleteFromManifest olddir
2013-11-24 19:53:15 +00:00
makeorigsymlink olddir
2013-11-24 16:49:03 +00:00
return (newdir </> "git-annex", deleteold)
installby a dstdir srcdir =
mapM_ (\x -> a (toRawFilePath x) (toRawFilePath (dstdir </> takeFileName x)))
=<< dirContents srcdir
#endif
2013-11-24 19:53:15 +00:00
sanitycheck dir =
2013-11-25 18:47:56 +00:00
unlessM (doesDirectoryExist dir) $
giveup $ "did not find " ++ dir ++ " in " ++ distributionfile
2013-11-24 19:53:15 +00:00
makeorigsymlink olddir = do
let origdir = fromRawFilePath (parentDir (toRawFilePath olddir)) </> installBase
removeWhenExistsWith R.removeLink (toRawFilePath origdir)
R.createSymbolicLink (toRawFilePath newdir) (toRawFilePath origdir)
{- Finds where the old version was installed. -}
oldVersionLocation :: IO FilePath
oldVersionLocation = readProgramFile >>= \case
Nothing -> giveup "Cannot find old distribution bundle; not upgrading."
Just pf -> do
let pdir = fromRawFilePath $ parentDir $ toRawFilePath pf
#ifdef darwin_HOST_OS
let dirs = splitDirectories pdir
{- It will probably be deep inside a git-annex.app directory. -}
let olddir = case findIndex ("git-annex.app" `isPrefixOf`) dirs of
Nothing -> pdir
Just i -> joinPath (take (i + 1) dirs)
#else
let olddir = pdir
#endif
when (null olddir) $
giveup $ "Cannot find old distribution bundle; not upgrading. (Looked in " ++ pdir ++ ")"
return olddir
2013-11-24 16:49:03 +00:00
{- Finds a place to install the new version.
- Generally, put it in the parent directory of where the old version was
- installed, and use a version number in the directory name.
- If unable to write to there, instead put it in the home directory.
-
- The directory is created. If it already exists, returns Nothing.
2013-11-24 16:49:03 +00:00
-}
newVersionLocation :: GitAnnexDistribution -> FilePath -> IO (Maybe FilePath)
newVersionLocation d olddir =
trymkdir newloc $ do
home <- myHomeDir
trymkdir (home </> s) $
return Nothing
2013-11-24 16:49:03 +00:00
where
s = installBase ++ "." ++ distributionVersion d
topdir = fromRawFilePath $ parentDir $ toRawFilePath olddir
2013-11-24 16:49:03 +00:00
newloc = topdir </> s
trymkdir dir fallback =
(createDirectory dir >> return (Just dir))
`catchIO` const fallback
installBase :: String
installBase = "git-annex." ++
#ifdef linux_HOST_OS
"linux"
#else
#ifdef darwin_HOST_OS
2013-11-24 19:53:15 +00:00
"app"
#else
"dir"
#endif
#endif
2013-11-24 16:49:03 +00:00
deleteFromManifest :: FilePath -> IO ()
deleteFromManifest dir = do
fs <- map (dir </>) . lines <$> catchDefaultIO "" (readFile manifest)
mapM_ (removeWhenExistsWith R.removeLink . toRawFilePath) fs
removeWhenExistsWith R.removeLink (toRawFilePath manifest)
2013-11-24 16:56:49 +00:00
removeEmptyRecursive dir
where
manifest = dir </> "git-annex.MANIFEST"
2013-11-24 16:56:49 +00:00
removeEmptyRecursive :: FilePath -> IO ()
removeEmptyRecursive dir = do
mapM_ removeEmptyRecursive =<< dirContents dir
void $ tryIO $ removeDirectory dir
2013-11-24 03:45:49 +00:00
{- This is a file that the UpgradeWatcher can watch for modifications to
- detect when git-annex has been upgraded.
-}
upgradeFlagFile :: IO FilePath
upgradeFlagFile = programPath
2013-11-24 03:45:49 +00:00
{- Sanity check to see if an upgrade is complete and the program is ready
- to be run. -}
upgradeSanityCheck :: IO Bool
upgradeSanityCheck = ifM usingDistribution
( doesFileExist =<< programFile
, do
-- Ensure that the program is present, and has no writers,
-- and can be run. This should handle distribution
-- upgrades, manual upgrades, etc.
program <- programPath
untilM (doesFileExist program <&&> nowriter program) $
threadDelaySeconds (Seconds 60)
boolSystem program [Param "version"]
2013-11-24 03:45:49 +00:00
)
where
nowriter f = null
. filter (`elem` [Lsof.OpenReadWrite, Lsof.OpenWriteOnly])
. map snd3
<$> Lsof.query [f]
usingDistribution :: IO Bool
usingDistribution = isJust <$> getEnv "GIT_ANNEX_STANDLONE_ENV"
downloadDistributionInfo :: Assistant (Maybe GitAnnexDistribution)
downloadDistributionInfo = do
uo <- liftAnnex Url.getUrlOptions
gpgcmd <- liftAnnex $ gpgCmd <$> Annex.getGitConfig
liftIO $ withTmpDir "git-annex.tmp" $ \tmpdir -> do
let infof = tmpdir </> "info"
let sigf = infof ++ ".sig"
ifM (isRight <$> Url.download nullMeterUpdate Nothing distributionInfoUrl infof uo
<&&> (isRight <$> Url.download nullMeterUpdate Nothing distributionInfoSigUrl sigf uo)
<&&> verifyDistributionSig gpgcmd sigf)
( parseInfoFile <$> readFileStrict infof
, return Nothing
)
distributionInfoUrl :: String
distributionInfoUrl = fromJust BuildInfo.upgradelocation ++ ".info"
distributionInfoSigUrl :: String
distributionInfoSigUrl = distributionInfoUrl ++ ".sig"
{- Upgrade only supported on linux and OSX. -}
upgradeSupported :: Bool
#ifdef linux_HOST_OS
upgradeSupported = isJust BuildInfo.upgradelocation
#else
#ifdef darwin_HOST_OS
upgradeSupported = isJust BuildInfo.upgradelocation
#else
upgradeSupported = False
#endif
#endif
{- Verifies that a file from the git-annex distribution has a valid
- signature. Pass the detached .sig file; the file to be verified should
- be located next to it.
-
- The gpg keyring used to verify the signature is located in
- trustedkeys.gpg, next to the git-annex program.
-}
verifyDistributionSig :: GpgCmd -> FilePath -> IO Bool
verifyDistributionSig gpgcmd sig = readProgramFile >>= \case
Just p | isAbsolute p ->
withUmask 0o0077 $ withTmpDir "git-annex-gpg.tmp" $ \gpgtmp -> do
let trustedkeys = takeDirectory p </> "trustedkeys.gpg"
boolGpgCmd gpgcmd
[ Param "--no-default-keyring"
, Param "--no-auto-check-trustdb"
, Param "--no-options"
, Param "--homedir"
, File gpgtmp
, Param "--keyring"
, File trustedkeys
, Param "--verify"
, File sig
]
_ -> return False