mywatch 0.1.0 → 0.1.1
raw patch · 7 files changed
+79/−28 lines, 7 files
Files
- ChangeLog.md +17/−0
- index.html +1/−1
- mywatch.cabal +2/−2
- src/Application.hs +17/−7
- src/Main.hs +10/−4
- src/Server.hs +3/−2
- static/mywatch.js +29/−12
ChangeLog.md view
@@ -1,3 +1,20 @@+0.1.1+=====++ * Request `HEAD /server/:server/processlist.json` before+ showing the server to user. This is to hide servers which+ are not allowed by Sproxy to this user.++ * Added a workaround for buggy haskell mysql package+ causing a heisenbug that random sections of the+ configuration files were not found by libmysqlclient.++ * Added a workaround for the way MariaDB's libmysqlclient+ processes SSL options. SSL now works with MariaDB's+ libmysqlclient.++ * Fixed parsing of `GRANT` queries (they have `NULL` states).+ 0.1.0 =====
index.html view
@@ -8,7 +8,7 @@ <link href="static/external/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="static/external/bootstrap/css/bootstrap-theme.min.css" rel="stylesheet"> <link href="static/mywatch.css" rel="stylesheet">- <title>MySQL Process Overview</title>+ <title>MyWatch</title> </head> <body>
mywatch.cabal view
@@ -1,5 +1,5 @@ name: mywatch-version: 0.1.0+version: 0.1.1 synopsis: View MySQL processes description: View queries on multiple MySQL servers. Designed to work behind Sproxy.@@ -26,7 +26,7 @@ executable mywatch default-language: Haskell2010- ghc-options: -Wall -static+ ghc-options: -Wall -static -threaded hs-source-dirs: src main-is: Main.hs other-modules:
src/Application.hs view
@@ -18,14 +18,14 @@ import Data.Text.Lazy (Text) import Database.MySQL.Simple (Connection, query_) import GHC.Generics (Generic)-import Network.HTTP.Types (notFound404)+import Network.HTTP.Types (ok200, notFound404, StdMethod(HEAD)) import Network.Wai (Application, Middleware) import Network.Wai.Middleware.RequestLogger (Destination(Handle), mkRequestLogger, RequestLoggerSettings(destination, outputFormat), OutputFormat(CustomOutputFormat)) import Network.Wai.Middleware.Static (addBase, hasPrefix, staticPolicy, (>->)) import System.IO (stderr)-import Web.Scotty (ScottyM, ActionM, middleware, json, file, get,+import Web.Scotty (ScottyM, ActionM, middleware, json, file, addroute, get, status, text, param, scottyApp) import qualified Data.HashMap.Lazy as HM @@ -51,8 +51,11 @@ get "/index.html" $ file index_html get "/serverlist.json" $ json (sort $ HM.keys ps)- get "/server/:server/processlist.json" $ apiGetProcesses ps+ get "/server/:server/processlist.json" $ apiGetProcessList ps + -- Used by client to see which servers are really allowed by Sproxy+ addroute HEAD "/server/:server/processlist.json" $ apiCanProcessList ps+ data Process = Process { id :: Int , user :: Text@@ -60,17 +63,24 @@ , db :: Maybe Text , command :: Text , time :: Int- , state :: Text+ , state :: Maybe Text , info :: Text } deriving (Generic) instance ToJSON Process -apiGetProcesses :: Pools -> ActionM ()-apiGetProcesses ps = do+apiCanProcessList :: Pools -> ActionM ()+apiCanProcessList ps = do server <- param "server" case HM.lookup server ps of Nothing -> status notFound404 >> text server- Just p -> do+ Just _ -> status ok200++apiGetProcessList :: Pools -> ActionM ()+apiGetProcessList ps = do+ server <- param "server"+ case HM.lookup server ps of+ Nothing -> status notFound404 >> text server+ Just p -> do res <- withDB p $ \c -> query_ c "SELECT \ \id, user, host, db, command, time, state, info \
src/Main.hs view
@@ -7,8 +7,9 @@ import Data.ByteString.Char8 (pack) import Data.Either.Utils (forceEither) import Data.Maybe (fromJust)+import Data.List (isPrefixOf) import Data.Version (showVersion)-import Database.MySQL.Base (ConnectInfo(..))+import Database.MySQL.Base (ConnectInfo(..), defaultSSLInfo) import Database.MySQL.Base.Types (Option(ReadDefaultFile, ReadDefaultGroup)) import Paths_mywatch (getDataDir, version) -- from cabal import System.Environment (getArgs)@@ -51,17 +52,22 @@ port = O.getArg args $ O.longOption "port" socket = fromJust $ O.getArg args $ O.longOption "socket" datadir = fromJust $ O.getArg args $ O.longOption "datadir"- servers <- filter ("client" /=) . Cf.sections . forceEither <$> Cf.readfile Cf.emptyCP file++ cf <- forceEither <$> Cf.readfile Cf.emptyCP file let+ servers = filter ("client" /=) . Cf.sections $ cf myInfo = map (\g -> ConnectInfo { connectDatabase = "", connectHost = "", connectPassword = "", connectPath = "", connectPort = 0,- connectSSL = Nothing,+ -- FIXME: https://jira.mariadb.org/browse/MDEV-10246+ connectSSL = if any (isPrefixOf "ssl") (forceEither $ Cf.options cf g)+ then Just defaultSSLInfo else Nothing, connectUser = "",- connectOptions = [ ReadDefaultFile file, ReadDefaultGroup (pack g) ]+ -- FIXME: Work aroung buggy mysql: unsafeUseAsCString creates garbage.+ connectOptions = [ ReadDefaultFile file, ReadDefaultGroup (pack $ g ++ "\0") ] }) servers listen = maybe (Right socket) (Left . read) port server listen myInfo datadir
src/Server.hs view
@@ -5,7 +5,6 @@ import Control.Exception.Base (throwIO, catch, bracket) import Data.Bits ((.|.))-import Data.ByteString.Lazy (fromStrict) import Data.List (find) import Data.Maybe (fromJust) import Data.Pool (createPool, destroyAllResources)@@ -22,6 +21,7 @@ import System.IO.Error (isDoesNotExistError) import System.Posix.Files (removeLink, setFileMode, socketMode, ownerReadMode, ownerWriteMode, groupReadMode, groupWriteMode)+import qualified Data.ByteString.Lazy as LBS import qualified Data.HashMap.Lazy as HM import qualified Database.MySQL.Simple as MySQL @@ -52,7 +52,8 @@ where isGroup (ReadDefaultGroup _) = True isGroup _ = False- getName (ReadDefaultGroup n) = fromStrict n+ -- FIXME: Removing trailing zero added for buggy mysql in Main.hs.+ getName (ReadDefaultGroup n) = LBS.takeWhile (0 /=) . LBS.fromStrict $ n getName _ = error "Cannot happen"
static/mywatch.js view
@@ -56,20 +56,37 @@ method: "GET", error: commonError, success: function(servers) {+ var total = servers.length;+ var available = [];+ var checked = 0; $.each(servers, function(i, s) {- $('#serverList>ul')- .append('<li><a href="#">' + s +- '</a></li>')- });- $("#serverList a").on("click", function() {- var server = $(this).text();- $(this).parent().parent().find('.active').removeClass('active');- $(this).parent().addClass('active');- clearInterval(interval);- getProcessList(server);- interval = setInterval(getProcessList, 60 * 1000, server);+ $.ajax({+ url: "server/" + s + "/processlist.json",+ method: "HEAD",+ success: function() {+ available.push(s);+ },+ complete: function() {+ var menu = $('#serverList>ul');+ checked++;+ if (checked === total) {+ $.each(available.sort(), function(i, s) {+ menu.append('<li><a href="#">' + s + '</a></li>')+ });+ $("#serverList a").on("click", function() {+ var server = $(this).text();+ document.title = server + ' — ' + 'MyWatch';+ $(this).parent().parent().find('.active').removeClass('active');+ $(this).parent().addClass('active');+ clearInterval(interval);+ getProcessList(server);+ interval = setInterval(getProcessList, 60 * 1000, server);+ });+ info.hide();+ }+ }+ }); });- info.hide(); } }); });