packages feed

hfd-0.0.2: src/hfd.hs

-- | Main module

module Main
(
main
)
where

import Data.Tuple.Utils (fst3)
import Data.Maybe
import Data.Word (Word32, Word16)
import Data.ByteString.Char8 (unpack)
import System.IO (Handle, hClose, hSetBinaryMode)
import System.Console.Haskeline (getInputLine)
import Control.Monad (unless, liftM)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.State (get)
import Control.Monad.IO.Class(liftIO, MonadIO)
import Control.Exception (bracket)
import Network (withSocketsDo, PortNumber, PortID(PortNumber), HostName,
                sClose, accept, listenOn)

import App (App, runApp, FileEntry(..), addFileEntry, getFileEntry,
            AppState(..), setLastCmd, setStack, Breakpoint(..), removeBreakpoint)
import IMsg (IMsg(..))
import UCmd (UCmd(..), parseUCmd, InfoCmd(..))
import Print (doPrint)
import Proto (setDebuggerOption, nextMsg, execContinue, execNext, execStep, execFinish,
              setBreakpoint, deleteBreakpoint)

-- | Entry point
main :: IO ()
main = withSocketsDo $ printHello >> bracket
  acceptPlayer
  (hClose . fst3)
  (start . fst3)
  where
  start h = do
    putStrLn "Enter \'help\' for list of commands"
    hSetBinaryMode h True
    runApp h app

-- | Print hello message
printHello :: IO ()
printHello = do
  putStrLn "HFB: Flash Debugger version 0.0.1"
  putStrLn "Copyright (c) 2011 Yuras Shumovich"
  putStrLn "mailto:shumovichy@gmail.com"

-- | Print list of commands
printHelp :: IO ()
printHelp = do
  printHello
  putStrLn "List of commands:"
  putStrLn "\thelp                          print this help"
  putStrLn "\tquit                          quit hfd"
  putStrLn "\tcontinue                      continue execution until breakpoint hit"
  putStrLn "\tstep                          continue execution until different source line reached"
  putStrLn "\tnext                          continue execution until next source line reached"
  putStrLn "\tinfo files                    show all source files"
  putStrLn "\tinfo breakpoints              show all breakpoints"
  putStrLn "\tbreakpoint <fileID>:<line>    set breakpoint at the location, e.g. \'b #1:23\'"
  putStrLn "\t                              use \'info files\' to get fileID"
  putStrLn "\tdelete <breakpointID>         delete breakpoint by ID"
  putStrLn "\t                              use \'info breakpoints\' to get breakpoint ID"
  putStrLn "\tdelete                        delete all breakpoints"
  putStrLn "\tprint <name>[.name]*          inspect variables"
  putStrLn "\tbacktrace (bt)                show call stack"
  putStrLn "Shortcuts are allowed, e.g. \'c\', \'co\', \'cont\', etc will mean \'continue\'"

-- | Listen on port, accept just one client and close socket
acceptPlayer :: IO (Handle, HostName, PortNumber)
acceptPlayer = bracket
  (listenOn (PortNumber 7935))
  sClose
  (\s -> putStrLn "Waiting for player..." >> accept s)

-- | Main app
app :: App IO ()
app = do
  processUntillBreak
  setDebuggerOption "break_on_fault" "on"
  -- doSetDebuggerOption "disable_script_stuck" "on"
  -- doSetDebuggerOption "disable_script_stuck_dialog" "on"
  -- doSetDebuggerOption "enumerate_override" "on"
  setDebuggerOption "notify_on_failure" "on"
  -- doSetDebuggerOption "invoke_setters" "on"
  -- doSetDebuggerOption "swf_load_messages" "on"
  loop
  where
  loop = do
    exit <- processUserInput
    unless exit (processUntillBreak >> loop)

-- | Process player's messages until 'IMsgBreakHitEx' received
processUntillBreak :: App IO ()
processUntillBreak = do
  msg <- nextMsg
  case msg of
    IMsgBreakHitEx _ _ stack   -> processBreak stack   >> printSourceLine msg
    IMsgSwdFileEntry _ _ _ _ _ -> processFileEntry msg >> processUntillBreak
    IMsgException _ _ _        -> processException msg >> processUntillBreak
    _                          -> processUntillBreak

-- | Save current stack
processBreak :: Monad m => [(Word16, Word16, Word32, String)] -> App m ()
processBreak = setStack . map toStack
  where
  toStack (fl, ln, _, fn) = (fromIntegral fl, fromIntegral ln, fn)

-- | Print information about exception
processException :: MonadIO m => IMsg -> App m ()
processException (IMsgException _ msg _) = do
  liftIO $ putStrLn " [exception]"
  liftIO $ putStrLn msg
processException _ = error "processException: something is wrong"

-- | Print current source line
printSourceLine :: IMsg -> App IO ()
printSourceLine (IMsgBreakHitEx file line _) = do
  files <- lift . lift $ fmap asFiles get
  let mln = srcLine files
  if isJust mln
    then liftIO $ putStrLn $ " " ++ show line ++ ": " ++ fromJust mln
    else liftIO $ putStrLn "No source"
  where
  srcLine files = do
    FileEntry _ content <- lookup (fromIntegral file) files
    let lln = take 1 $ drop (fromIntegral line - 1) content
    if null lln
      then Nothing
      else Just $ head lln
printSourceLine _ = error "printSourceLine: something is wrong..."

-- | Read file content and add new file entry
processFileEntry :: MonadIO m => IMsg -> App m ()
processFileEntry (IMsgSwdFileEntry idi _ nm _ _) = do
  content <- liftIO readFile'
  addFileEntry (fromIntegral idi, FileEntry name (lines content))
  where
  name = unpack nm
  path = map fixup name
  fixup ';' = '/'
  fixup ch = ch
  readFile' = catch (readFile path) (const $ return "")
processFileEntry _ = error "processFileEntry: something is wrong..."

-- | Read user command and process it
processUserInput :: App IO Bool
processUserInput = do
  l <- lift $ getInputLine "hfb> "
  let cmd = l >>= parseUCmd
  setLastCmd cmd
  if isNothing cmd
    then liftIO (putStrLn "Unknown command") >> processUserInput
    else processCmd (fromJust cmd)

-- | Actualy process user command
processCmd :: UCmd         -- ^ User command
           -> App IO Bool  -- ^ whether to exit
processCmd UCmdEmpty      = do
  cmd <- lift . lift $ liftM asLastCmd get
  if isJust cmd
    then processCmd (fromJust cmd)
    else processUserInput
processCmd UCmdQuit               = return True
processCmd UCmdContinue           = execContinue >> return False
processCmd UCmdStep               = execStep >> return False
processCmd UCmdNext               = execNext >> return False
processCmd UCmdFinish             = execFinish >> return False
processCmd (UCmdInfo cmd)         = processInfoCmd cmd >> processUserInput
processCmd (UCmdPrint v)          = doPrint v >> processUserInput
processCmd (UCmdBreakpoint fl ln) = setBreakpoint fl ln >> processUserInput
processCmd UCmdStack              = printStack >> processUserInput
processCmd UCmdList               = listSource >> processUserInput
processCmd (UCmdDelete (Just iD)) = deleteBP iD >> processUserInput
processCmd (UCmdDelete Nothing)   = deleteAll >> processUserInput
processCmd UCmdTest               = processUserInput
processCmd UCmdHelp               = liftIO printHelp >> processUserInput

-- | Delete breakpoint by id
deleteBP :: MonadIO m => Int -> App m ()
deleteBP iD = do
  bs <- lift . lift $ liftM asBreaks get
  let bp = lookup iD bs
  if isJust bp
    then do
      let Breakpoint fl ln = fromJust bp
      deleteBreakpoint fl ln
      removeBreakpoint iD
    else liftIO $ putStrLn "Unknown breakpoint id. Type \"info breakpoints\" for list of all breakpoints"

-- | Delete all breakpoints
deleteAll :: MonadIO m => App m ()
deleteAll = do
  bs <- lift . lift $ liftM asBreaks get
  mapM_ (deleteBP . fst) bs

-- | Print source around current position
listSource :: MonadIO m => App m ()
listSource = do
  stack <- lift . lift $ liftM asStack get
  if null stack
    then liftIO $ putStrLn "No source"
    else print' $ head stack
  where
  print' (fl, ln, _) = do
    fs <- lift . lift $ liftM asFiles get
    let f = lookup fl fs
    if isNothing f
      then liftIO $ putStrLn "No source"
      else liftIO $ mapM_ printLine $ take 11 $ drop (ln - 6) (zip allLines $ feContent $ fromJust f)
  printLine (ln, cont) = putStrLn $ " " ++ show ln ++ ": " ++ cont
  allLines :: [Int]
  allLines = [1..]

-- | Print current stack
printStack :: MonadIO m => App m ()
printStack = do
  state <- lift . lift $ get
  let stack = asStack state
  let files = asFiles state
  liftIO $ mapM_ (print' files) stack
  where
  print' fs (fl, ln, fn) = putStrLn $ fn ++ "() at " ++ file fs fl ++ ":" ++ show ln
  file fs fl = let fe = lookup fl fs in
    if isJust fe
      then fePath (fromJust fe)
      else "(no source)"

-- | Process @info@ command
processInfoCmd :: MonadIO m => InfoCmd -> App m ()
processInfoCmd ICFiles = printFiles
  where
  printFiles = do
    files <- lift . lift $ liftM asFiles get
    liftIO $ mapM_ printFile files
  printFile (idi, FileEntry name _) =
    putStrLn $ "#" ++ show idi ++ ": " ++ name
processInfoCmd ICBreakpoints = do
  bs <- lift . lift $ liftM asBreaks get
  mapM_ printBP bs
  where
  printBP (iD, Breakpoint fl ln) = do
    fe <- liftM fromJust $ getFileEntry fl
    liftIO $ putStrLn $ " " ++ show iD ++ "\t: " ++ fePath fe ++ " at line " ++ show ln