imp: cli,ui,web: support ghc-debug for analysing memory/profile info

When built with the ghcdebug flag and started with --debug=-1 (or -2
to pause at startup, or -3 to pause before exit), hledger can be
controlled by ghc-debug clients like ghc-debug-brick or a custom
ghc-debug query script.

Also, refactor version string code.
This commit is contained in:
Simon Michael 2024-04-25 06:28:11 -10:00
parent e1bcbc3238
commit d17b32c7eb
13 changed files with 187 additions and 66 deletions

View File

@ -92,6 +92,8 @@ val
-- http://hackage.haskell.org/packages/archive/htrace/0.1/doc/html/Debug-HTrace.html -- http://hackage.haskell.org/packages/archive/htrace/0.1/doc/html/Debug-HTrace.html
-- http://hackage.haskell.org/packages/archive/traced/2009.7.20/doc/html/Debug-Traced.html -- http://hackage.haskell.org/packages/archive/traced/2009.7.20/doc/html/Debug-Traced.html
-- https://hackage.haskell.org/package/debug -- https://hackage.haskell.org/package/debug
{-# LANGUAGE CPP #-}
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
module Hledger.Utils.Debug ( module Hledger.Utils.Debug (
@ -161,7 +163,12 @@ module Hledger.Utils.Debug (
,dbg9With ,dbg9With
-- * Utilities -- * Utilities
,GhcDebugMode(..)
,ghcDebugMode
,withGhcDebug'
,ghcDebugPause'
,lbl_ ,lbl_
,progName
-- * Re-exports -- * Re-exports
-- ,module Debug.Breakpoint -- ,module Debug.Breakpoint
@ -176,6 +183,9 @@ import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.List hiding (uncons) import Data.List hiding (uncons)
-- import Debug.Breakpoint -- import Debug.Breakpoint
import Debug.Trace (trace, traceIO, traceShowId) import Debug.Trace (trace, traceIO, traceShowId)
#ifdef GHCDEBUG
import GHC.Debug.Stub (pause, withGhcDebug)
#endif
import Safe (readDef) import Safe (readDef)
import System.Environment (getProgName) import System.Environment (getProgName)
import System.Exit (exitFailure) import System.Exit (exitFailure)
@ -212,6 +222,55 @@ debugLevel = case dropWhile (/="--debug") progArgs of
['-':'-':'d':'e':'b':'u':'g':'=':v] -> readDef 1 v ['-':'-':'d':'e':'b':'u':'g':'=':v] -> readDef 1 v
_ -> 0 _ -> 0
-- | Whether ghc-debug support is included in this build, and if so, how it will behave.
-- When hledger is built with the ghcdebug cabal build flag (normally disabled),
-- it can listen (on unix ?) for connections from ghc-debug clients like ghc-debug-brick,
-- for pausing/resuming the program and inspecting memory usage and profile information.
--
-- This is enabled by running hledger with a negative --debug level, with three different modes:
-- --debug=-1 - run normally (can be paused/resumed by a ghc-debug client),
-- --debug=-2 - pause and await client commands at program start (not useful currently),
-- --debug=-3 - pause and await client commands at program end.
data GhcDebugMode =
GDNotSupported
| GDDisabled
| GDNoPause
| GDPauseAtStart
| GDPauseAtEnd
-- keep synced with ghcDebugMode
deriving (Eq,Ord)
-- | Should the program open a socket allowing control by ghc-debug-brick or similar ghc-debug client ?
-- See GhcDebugMode.
ghcDebugMode :: GhcDebugMode
ghcDebugMode =
case debugLevel of
-- keep synced with GhcDebugMode
#ifdef GHCDEBUG
(-1) -> GDNoPause
(-2) -> GDPauseAtStart
(-3) -> GDPauseAtEnd
#endif
_ -> GDDisabled
-- | When ghc-debug support has been built into the program and enabled at runtime with --debug=-N,
-- this calls ghc-debug's withGhcDebug; otherwise it's a no-op.
withGhcDebug' =
#ifdef GHCDEBUG
if ghcDebugMode > GDDisabled then withGhcDebug else id
#else
id
#endif
-- | When ghc-debug support has been built into the program, this calls ghc-debug's pause, otherwise it's a no-op.
ghcDebugPause' :: IO ()
ghcDebugPause' =
#ifdef GHCDEBUG
pause
#else
return ()
#endif
-- | Trace a value with the given show function before returning it. -- | Trace a value with the given show function before returning it.
traceWith :: (a -> String) -> a -> a traceWith :: (a -> String) -> a -> a
traceWith f a = trace (f a) a traceWith f a = trace (f a) a

View File

@ -58,7 +58,9 @@ writeChan = BC.writeBChan
hledgerUiMain :: IO () hledgerUiMain :: IO ()
hledgerUiMain = withProgName "hledger-ui.log" $ do -- force Hledger.Utils.Debug.* to log to hledger-ui.log hledgerUiMain = withGhcDebug' $ withProgName "hledger-ui.log" $ do -- force Hledger.Utils.Debug.* to log to hledger-ui.log
when (ghcDebugMode == GDPauseAtStart) $ ghcDebugPause'
traceLogAtIO 1 "\n\n\n\n==== hledger-ui start" traceLogAtIO 1 "\n\n\n\n==== hledger-ui start"
dbg1IO "args" progArgs dbg1IO "args" progArgs
dbg1IO "debugLevel" debugLevel dbg1IO "debugLevel" debugLevel
@ -80,6 +82,8 @@ hledgerUiMain = withProgName "hledger-ui.log" $ do -- force Hledger.Utils.Debug
-- _ | boolopt "binary-filename" rawopts -> putStrLn (binaryfilename progname) -- _ | boolopt "binary-filename" rawopts -> putStrLn (binaryfilename progname)
_ -> withJournalDo copts' (runBrickUi opts) _ -> withJournalDo copts' (runBrickUi opts)
when (ghcDebugMode == GDPauseAtEnd) $ ghcDebugPause'
runBrickUi :: UIOpts -> Journal -> IO () runBrickUi :: UIOpts -> Journal -> IO ()
runBrickUi uopts0@UIOpts{uoCliOpts=copts@CliOpts{inputopts_=_iopts,reportspec_=rspec@ReportSpec{_rsReportOpts=ropts}}} j = runBrickUi uopts0@UIOpts{uoCliOpts=copts@CliOpts{inputopts_=_iopts,reportspec_=rspec@ReportSpec{_rsReportOpts=ropts}}} j =
do do

View File

@ -1,4 +1,5 @@
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
{-# LANGUAGE TemplateHaskell #-}
{-| {-|
-} -}
@ -10,6 +11,7 @@ import Data.Default (def)
import Data.List (intercalate) import Data.List (intercalate)
import qualified Data.Map as M import qualified Data.Map as M
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import GitHash (tGitInfoCwdTry)
import Lens.Micro (set) import Lens.Micro (set)
import System.Environment (getArgs) import System.Environment (getArgs)
@ -29,8 +31,20 @@ packageversion =
progname :: ProgramName progname :: ProgramName
progname = "hledger-ui" progname = "hledger-ui"
prognameandversion :: VersionString -- | Generate the version string for this program.
prognameandversion = versionString progname packageversion -- The template haskell call is here rather than in Hledger.Cli.Version to avoid wasteful recompilation.
prognameandversion :: String
prognameandversion =
versionStringWith
$$tGitInfoCwdTry
#ifdef GHCDEBUG
True
#else
False
#endif
progname
packageversion
uiflags = [ uiflags = [
-- flagNone ["debug-ui"] (setboolopt "rules-file") "run with no terminal output, showing console" -- flagNone ["debug-ui"] (setboolopt "rules-file") "run with no terminal output, showing console"

View File

@ -35,8 +35,12 @@ extra-source-files:
flags: flags:
threaded: threaded:
description: Build with support for multithreaded execution description: Build with support for multithreaded execution
manual: false
default: true default: true
manual: false
ghcdebug:
description: Build with support for attaching a ghc-debug client
default: false
manual: true
cpp-options: -DVERSION="1.33.99" cpp-options: -DVERSION="1.33.99"
@ -51,6 +55,12 @@ ghc-options:
dependencies: dependencies:
- base >=4.14 && <4.20 - base >=4.14 && <4.20
when:
- condition: (flag(ghcdebug))
cpp-options: -DGHCDEBUG
dependencies:
- ghc-debug-stub >=0.6.0.0 && <0.7
# curses is required to build terminfo for vty for hledger-ui. # curses is required to build terminfo for vty for hledger-ui.
# On POSIX systems it might be not present. # On POSIX systems it might be not present.
# On windows it's very likely not present, but possibly it could be. # On windows it's very likely not present, but possibly it could be.
@ -86,6 +96,7 @@ library:
- extra >=1.6.3 - extra >=1.6.3
- filepath - filepath
- fsnotify >=0.4 && <0.5 - fsnotify >=0.4 && <0.5
- githash >=0.1.6.2
- microlens >=0.4 - microlens >=0.4
- microlens-platform >=0.2.3.1 - microlens-platform >=0.2.3.1
- megaparsec >=7.0.0 && <9.7 - megaparsec >=7.0.0 && <9.7

View File

@ -46,7 +46,9 @@ hledgerWebDev =
-- Run normally. -- Run normally.
hledgerWebMain :: IO () hledgerWebMain :: IO ()
hledgerWebMain = do hledgerWebMain = withGhcDebug' $ do
when (ghcDebugMode == GDPauseAtStart) $ ghcDebugPause'
-- try to encourage user's $PAGER to properly display ANSI (in command line help) -- try to encourage user's $PAGER to properly display ANSI (in command line help)
when useColorOnStdout setupPager when useColorOnStdout setupPager
@ -63,6 +65,8 @@ hledgerWebMain = do
(`withArgs` hledgerWebTest) . filter (`notElem` ["--test","--"]) =<< getArgs (`withArgs` hledgerWebTest) . filter (`notElem` ["--test","--"]) =<< getArgs
| otherwise -> withJournalDo copts (web wopts) | otherwise -> withJournalDo copts (web wopts)
when (ghcDebugMode == GDPauseAtEnd) $ ghcDebugPause'
-- | The hledger web command. -- | The hledger web command.
web :: WebOpts -> Journal -> IO () web :: WebOpts -> Journal -> IO ()
web opts j = do web opts j = do

View File

@ -1,5 +1,6 @@
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Hledger.Web.WebOptions where module Hledger.Web.WebOptions where
@ -9,6 +10,7 @@ import Data.ByteString.UTF8 (fromString)
import Data.Default (Default(def)) import Data.Default (Default(def))
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Data.Text (Text) import Data.Text (Text)
import GitHash (tGitInfoCwdTry)
import System.Environment (getArgs) import System.Environment (getArgs)
import Network.Wai as WAI import Network.Wai as WAI
import Network.Wai.Middleware.Cors import Network.Wai.Middleware.Cors
@ -29,12 +31,23 @@ packageversion =
"" ""
#endif #endif
-- | The name of this program's executable.
progname :: ProgramName progname :: ProgramName
progname = "hledger-web" progname = "hledger-web"
prognameandversion :: VersionString -- | Generate the version string for this program.
prognameandversion = versionString progname packageversion -- The template haskell call is here rather than in Hledger.Cli.Version to avoid wasteful recompilation.
prognameandversion :: String
prognameandversion =
versionStringWith
$$tGitInfoCwdTry
#ifdef GHCDEBUG
True
#else
False
#endif
progname
packageversion
webflags :: [Flag RawOpts] webflags :: [Flag RawOpts]
webflags = webflags =

View File

@ -49,16 +49,20 @@ extra-source-files:
flags: flags:
library-only: library-only:
description: Build for use with "yesod devel" description: Build for use with "yesod devel"
manual: false
default: false default: false
manual: false
dev: dev:
description: Turn on development settings, like auto-reload templates. description: Turn on development settings, like auto-reload templates.
manual: false
default: false default: false
manual: false
threaded: threaded:
description: Build with support for multithreaded execution. description: Build with support for multithreaded execution.
manual: false
default: true default: true
manual: false
ghcdebug:
description: Build with support for attaching a ghc-debug client
default: false
manual: true
ghc-options: ghc-options:
- -Wall - -Wall
@ -76,6 +80,10 @@ when:
# 'ghc-options: -O0' is not needed. Use the --disable-optimization configure flag. # 'ghc-options: -O0' is not needed. Use the --disable-optimization configure flag.
- condition: flag(dev) - condition: flag(dev)
ghc-options: -O0 ghc-options: -O0
- condition: (flag(ghcdebug))
cpp-options: -DGHCDEBUG
dependencies:
- ghc-debug-stub >=0.6.0.0 && <0.7
dependencies: dependencies:
- base >=4.14 && <4.20 - base >=4.14 && <4.20
@ -109,6 +117,7 @@ library:
- directory >=1.2.3.0 - directory >=1.2.3.0
- extra >=1.6.3 - extra >=1.6.3
- filepath - filepath
- githash >=0.1.6.2
- hjsmin - hjsmin
- http-conduit - http-conduit
- http-client - http-client

View File

@ -1,8 +1,3 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
{-# HLINT ignore "Unused LANGUAGE pragma" #-}
{-| {-|
This is the root module of the @hledger@ package, This is the root module of the @hledger@ package,
@ -69,9 +64,12 @@ etc.
-} -}
{-# LANGUAGE LambdaCase #-}
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
{-# HLINT ignore "Unused LANGUAGE pragma" #-}
module Hledger.Cli ( module Hledger.Cli (
prognameandversion,
versionString,
main, main,
mainmode, mainmode,
argsToCliOpts, argsToCliOpts,
@ -90,7 +88,9 @@ where
import Control.Monad (when) import Control.Monad (when)
import Data.List import Data.List
import qualified Data.List.NonEmpty as NE import qualified Data.List.NonEmpty as NE
import Data.Time.Clock.POSIX (getPOSIXTime)
import Safe import Safe
import System.Console.CmdArgs.Explicit hiding (Name) -- don't clash with hledger-ui
import qualified System.Console.CmdArgs.Explicit as C import qualified System.Console.CmdArgs.Explicit as C
import System.Environment import System.Environment
import System.Exit import System.Exit
@ -98,12 +98,6 @@ import System.FilePath
import System.Process import System.Process
import Text.Printf import Text.Printf
import Data.Time.Clock.POSIX (getPOSIXTime)
import GitHash (tGitInfoCwdTry)
import System.Console.CmdArgs.Explicit hiding (Name) -- don't clash with hledger-ui
import Hledger import Hledger
import Hledger.Cli.CliOptions import Hledger.Cli.CliOptions
import Hledger.Cli.Commands import Hledger.Cli.Commands
@ -112,24 +106,6 @@ import Hledger.Cli.Utils
import Hledger.Cli.Version import Hledger.Cli.Version
-- | The program name and version string for this build of the hledger tool,
-- including any git info available at build time.
prognameandversion :: String
prognameandversion = versionString progname packageversion
-- | A helper to generate the best version string we can from the given
-- program name and package version strings, current os and architecture,
-- and any git info available at build time (commit hash, commit date, branch
-- name, patchlevel since latest release tag for that program's package).
-- Typically called for programs "hledger", "hledger-ui", or "hledger-web".
--
-- The git info changes whenever any file in the repository changes.
-- Keeping this template haskell call here and not down in Hledger.Cli.Version
-- helps reduce the number of modules recompiled.
versionString :: ProgramName -> PackageVersion -> String
versionString = versionStringWith $$tGitInfoCwdTry
-- | The overall cmdargs mode describing hledger's command-line options and subcommands. -- | The overall cmdargs mode describing hledger's command-line options and subcommands.
mainmode addons = defMode { mainmode addons = defMode {
modeNames = [progname ++ " [CMD]"] modeNames = [progname ++ " [CMD]"]
@ -172,8 +148,11 @@ mainmode addons = defMode {
-- | Let's go! -- | Let's go!
main :: IO () main :: IO ()
main = do main = withGhcDebug' $ do
when (ghcDebugMode == GDPauseAtStart) $ ghcDebugPause'
starttime <- getPOSIXTime starttime <- getPOSIXTime
-- try to encourage user's $PAGER to properly display ANSI -- try to encourage user's $PAGER to properly display ANSI
when useColorOnStdout setupPager when useColorOnStdout setupPager
@ -286,6 +265,8 @@ main = do
runHledgerCommand runHledgerCommand
when (ghcDebugMode == GDPauseAtEnd) $ ghcDebugPause'
-- | Parse hledger CLI options from these command line arguments and -- | Parse hledger CLI options from these command line arguments and
-- add-on command names, or raise any error. -- add-on command names, or raise any error.
argsToCliOpts :: [String] -> [String] -> IO CliOpts argsToCliOpts :: [String] -> [String] -> IO CliOpts

View File

@ -15,6 +15,8 @@ related utilities used by hledger commands.
{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeOperators #-}
module Hledger.Cli.CliOptions ( module Hledger.Cli.CliOptions (
progname,
prognameandversion,
-- * cmdargs flags & modes -- * cmdargs flags & modes
helpflags, helpflags,
@ -87,6 +89,7 @@ import Data.Maybe
-- import Data.Text (Text) -- import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Void (Void) import Data.Void (Void)
import GitHash (tGitInfoCwdTry)
import Safe import Safe
import String.ANSI import String.ANSI
import System.Console.CmdArgs hiding (Default,def) import System.Console.CmdArgs hiding (Default,def)
@ -110,6 +113,24 @@ import Data.Time.Clock.POSIX (POSIXTime)
import Data.List (isPrefixOf, isSuffixOf) import Data.List (isPrefixOf, isSuffixOf)
-- | The name of this program's executable.
progname :: ProgramName
progname = "hledger"
-- | Generate the version string for this program.
-- The template haskell call is here rather than in Hledger.Cli.Version to avoid wasteful recompilation.
prognameandversion :: String
prognameandversion =
versionStringWith
$$tGitInfoCwdTry
#ifdef GHCDEBUG
True
#else
False
#endif
progname
packageversion
-- common cmdargs flags -- common cmdargs flags
-- keep synced with flag docs in doc/common.m4 -- keep synced with flag docs in doc/common.m4

View File

@ -65,7 +65,6 @@ import Test.Tasty (defaultMain)
import Hledger import Hledger
import Hledger.Cli.CliOptions import Hledger.Cli.CliOptions
import Hledger.Cli.Version
import Hledger.Cli.Commands.Accounts import Hledger.Cli.Commands.Accounts
import Hledger.Cli.Commands.Activity import Hledger.Cli.Commands.Activity
import Hledger.Cli.Commands.Add import Hledger.Cli.Commands.Add

View File

@ -9,7 +9,6 @@ module Hledger.Cli.Version (
VersionString, VersionString,
packageversion, packageversion,
packagemajorversion, packagemajorversion,
progname,
versionStringWith, versionStringWith,
) )
where where
@ -39,10 +38,6 @@ packageversion =
packagemajorversion :: PackageVersion packagemajorversion :: PackageVersion
packagemajorversion = intercalate "." $ take 2 $ splitAtElement '.' packageversion packagemajorversion = intercalate "." $ take 2 $ splitAtElement '.' packageversion
-- | The name of this package's main executable.
progname :: ProgramName
progname = "hledger"
-- | Given possible git state info from the build directory (or an error message, which is ignored), -- | Given possible git state info from the build directory (or an error message, which is ignored),
-- the name of a program (executable) in the currently building package, -- the name of a program (executable) in the currently building package,
-- and the package's version, make a complete version string. Here is the logic: -- and the package's version, make a complete version string. Here is the logic:
@ -55,6 +50,7 @@ progname = "hledger"
-- * (TODO, requires adding --match support to githash: -- * (TODO, requires adding --match support to githash:
-- If there are tags matching THISPKG-[0-9]*, the latest one is used to calculate patch level -- If there are tags matching THISPKG-[0-9]*, the latest one is used to calculate patch level
-- (number of commits since tag), and if non-zero, it and the branch name are shown.) -- (number of commits since tag), and if non-zero, it and the branch name are shown.)
-- * If the ghcdebug build flag was enabled, "ghc-debug support" is shown.
-- --
-- Some example outputs: -- Some example outputs:
-- --
@ -65,9 +61,19 @@ progname = "hledger"
-- This function requires git log to show the default (rfc2822-style) date format, -- This function requires git log to show the default (rfc2822-style) date format,
-- so that must not be overridden by a log.date git config variable. -- so that must not be overridden by a log.date git config variable.
-- --
versionStringWith :: Either String GitInfo -> ProgramName -> PackageVersion -> VersionString -- The GitInfo if any, fetched by template haskell, is passed down from
versionStringWith egitinfo prognam packagever = -- a top-level module, reducing wasteful recompilation.
concat [ prognam , " " , version , ", " , os' , "-" , arch ] -- The status of the ghcdebug build flag is also passed down, since it is
-- specific to each hledger package.
--
-- This is used indirectly by at least hledger, hledger-ui, and hledger-web,
-- so output should be suitable for all of those.
--
versionStringWith :: Either String GitInfo -> Bool -> ProgramName -> PackageVersion -> VersionString
versionStringWith egitinfo ghcdebug progname packagever =
concat $
[ progname , " " , version , ", " , os' , "-" , arch ]
++ [ " with ghc-debug support" | ghcdebug ]
where where
os' | os == "darwin" = "mac" os' | os == "darwin" = "mac"
| os == "mingw32" = "windows" | os == "mingw32" = "windows"
@ -102,14 +108,3 @@ versionStringWith egitinfo prognam packagever =
dd = (if length day < 2 then ('0':) else id) day dd = (if length day < 2 then ('0':) else id) day
-- but could be overridden by a log.date config variable in repo or user git config -- but could be overridden by a log.date config variable in repo or user git config
_ -> packageversion _ -> packageversion
-- -- | Given a program name, return a precise platform-specific executable
-- -- name suitable for naming downloadable binaries. Can raise an error if
-- -- the version and patch level was not defined correctly at build time.
-- binaryfilename :: String -> String
-- binaryfilename progname = concat
-- [progname, "-", buildversion, "-", os', "-", arch, suffix]
-- where
-- (os',suffix) | os == "darwin" = ("mac","" :: String)
-- | os == "mingw32" = ("windows",".exe")
-- | otherwise = (os,"")

View File

@ -80,13 +80,17 @@ extra-source-files:
flags: flags:
terminfo: terminfo:
description: On POSIX systems, build with the terminfo lib for detecting terminal width. description: On POSIX systems, build with the terminfo lib for detecting terminal width
manual: false
default: true default: true
manual: false
threaded: threaded:
description: Build with support for multithreaded execution description: Build with support for multithreaded execution
manual: false
default: true default: true
manual: false
ghcdebug:
description: Build with support for attaching a ghc-debug client
default: false
manual: true
ghc-options: ghc-options:
- -Wall - -Wall
@ -139,6 +143,10 @@ when:
- condition: (!(os(windows))) && (flag(terminfo)) - condition: (!(os(windows))) && (flag(terminfo))
dependencies: dependencies:
- terminfo - terminfo
- condition: (flag(ghcdebug))
cpp-options: -DGHCDEBUG
dependencies:
- ghc-debug-stub >=0.6.0.0 && <0.7
library: library:
cpp-options: -DVERSION="1.33.99" cpp-options: -DVERSION="1.33.99"

View File

@ -12,6 +12,9 @@ extra-deps:
- process-1.6.19.0 # for HSEC-2024-0003 - process-1.6.19.0 # for HSEC-2024-0003
- haskeline-0.8.2.1 - haskeline-0.8.2.1
- ghc-debug-convention-0.6.0.0
- ghc-debug-stub-0.6.0.0
nix: nix:
pure: false pure: false
packages: [perl gmp ncurses zlib] packages: [perl gmp ncurses zlib]