
-- | This code uses @sysctl@ and @KERN_PROC_PATHNAME@,
-- if we are on FreeBSD 6.0 or newer, and falls back to procfs
-- on older FreeBSD-s.

{-# LANGUAGE CPP, ForeignFunctionInterface #-}
module System.Environment.Executable.FreeBSD
  ( getExecutablePath  
  , getPID
  )
  where
  
import Data.Bits
import Data.Word
import Data.Int

import Data.Char
import Data.List

import Control.Monad

import Foreign
import Foreign.C

import System.Posix
import System.Directory

--------------------------------------------------------------------------------

#define CTL_KERN             1 
#define KERN_PROC           14    
#define KERN_PROC_PATHNAME  12   
-- KERN_PROC_PATHNAME exists from FreeBSD 6.0

#define ENOMEM    12

#define KERN_OSTYPE    1
#define KERN_OSRELEASE 2
#define KERN_OSREV     3
-- KERN_OSREV gives back a totally random-looking
-- number with a totally undocumented meaning, yeah, fuck that.

#define KERN_VERSION   4

--------------------------------------------------------------------------------

-- the only data point is the string "6.4-RELEASE"
-- yay for undocumented queries!
-- not let's try to parse that
parseOSRelease :: String -> Maybe (Int,Int)
parseOSRelease text = 
  if major /= "" && temp1 /= "" && minor /= "" && dot == '.'
    then Just (read major, read minor)
    else Nothing
  where
    (major,temp1) = span isDigit text
    (dot:temp2)   = temp1
    (minor,rest)  = span isDigit temp2

getExecutablePath :: IO FilePath
getExecutablePath = do
{-
  osrev <- getKernInt KERN_OSREV
  ostype <- getKernString KERN_OSTYPE 256
  kver  <- getKernString KERN_VERSION 256
  print osrev
  print ostype
  print kver
-}

  osrel  <- getKernString KERN_OSRELEASE 256
--  print osrel
--  print $ parseOSRelease osrel
  case parseOSRelease osrel of
    Just (maj,_) -> if maj >= 6
      then getExecutablePathSysCtl 256
      else getExecutablePathProcFS
    Nothing -> getExecutablePathProcFS

--------------------------------------------------------------------------------

-- int sysctl(int *name, u_int namelen, void *oldp, size_t *oldlenp, void *newp, size_t newlen);
foreign import ccall "sys/sysctl.h sysctl" 
  sysctl :: Ptr CInt -> CUInt -> Ptr a -> Ptr CSize -> Ptr a -> CSize -> IO CInt

-- brrrrrrr...
foreign import ccall "errno.h &errno" errno :: Ptr CInt
      
getKernString :: CInt -> Int -> IO String
getKernString what len = do
  allocaArray 2 $ \mib -> do
    pokeArray mib [ CTL_KERN, what ]  
    alloca $ \buflen -> do
      poke buflen (fromIntegral len :: CSize)
      allocaBytes len $ \buf -> do
        sysctl mib 2 buf buflen nullPtr 0
        peekCString buf

getKernInt :: CInt -> IO CInt
getKernInt what = do
  allocaArray 2 $ \mib -> do
    pokeArray mib [ CTL_KERN, what ]  
    alloca $ \posrev -> do
      alloca $ \buflen -> do
        poke buflen (fromIntegral (sizeOf (undefined :: CInt)) :: CSize)
        sysctl mib 2 posrev buflen nullPtr 0
        peek posrev
      
getExecutablePathSysCtl :: Int -> IO FilePath
getExecutablePathSysCtl size = do
  allocaArray 4 $ \mib -> do
    pokeArray mib [ CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1 ]   -- @-1@ is current process (could be a PID)
    allocaBytes size $ \buf -> do
      alloca $ \buflen -> do
        poke buflen (fromIntegral size :: CSize)
        err <- sysctl mib 4 buf buflen nullPtr 0
        case err of
          0 -> peekCString buf
          -1 -> do
            errn <- peek errno
            case errn of
              ENOMEM -> getExecutablePathSysCtl (size*2)
              _ -> error "getExecutablePath: unknown system error"          

{-
int mib[4];
mib[0] = CTL_KERN;
mib[1] = KERN_PROC;
mib[2] = KERN_PROC_PATHNAME;
mib[3] = -1;
char buf[1024];
size_t cb = sizeof(buf);
sysctl(mib, 4, buf, &cb, NULL, 0);
-}

--------------------------------------------------------------------------------

-- procfs fallback for FreeBSD < 6.0

getPID :: IO Int
getPID = liftM fromIntegral $ getProcessID

{-
getExecutablePathProcFS :: IO FilePath
getExecutablePathProcFS = do
  pid <- getPID
  let procPid = "/proc/" ++ show pid ++ "/file"
  fname <- readSymbolicLink procPid
  return fname
-}

getExecutablePathProcFS :: IO FilePath
getExecutablePathProcFS = do
  try1 <- getExecutablePathProcFS' "file"
  case try1 of
    Just xx -> return xx
    Nothing -> do
      try2 <- getExecutablePathUnderscoreFallback
      case try2 of
        Just yy -> return yy
        Nothing -> error "getExecutablePath/FreeBSD: unable to obtain the path"
      
-- eg. @getExecutablePathProcFS "file"@
getExecutablePathProcFS' :: FilePath -> IO (Maybe FilePath)
getExecutablePathProcFS' symlink = do
  pid <- getPID
  let procPid  = "/proc/" ++ show pid ++ "/" ++ symlink
  fileExist procPid >>= \b -> if b 
    then getSymbolicLinkStatus procPid >>= \s -> if isSymbolicLink s
      then liftM Just $ readSymbolicLink procPid
      else return Nothing
    else return Nothing

--------------------------------------------------------------------------------

-- even more fallback, if for some reason procfs doesn't work
      
-- this is an unreliable fallback trying to 
-- get the environment variable named "_".  
getExecutablePathUnderscoreFallback :: IO (Maybe FilePath) 
getExecutablePathUnderscoreFallback = do
  mp <- getEnv "_" 
  case mp of
    Nothing -> return mp
    Just p -> do
      q <- canonicalizePath p
      return (Just q)  

--------------------------------------------------------------------------------


