Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to work with directories/ how to work with processes/ how to do things concurrently #22

Open
friedbrice opened this issue Oct 2, 2019 · 3 comments
Labels
new example This issue is about writing a new example program.

Comments

@friedbrice
Copy link
Contributor

This is how I check my git repos every morning. This is a pretty long example, so it probably needs to be split into three smaller examples (which I'm happy to do if you think it'll make for good content).

#!/usr/bin/env stack
{- stack script --resolver lts-13.26 -}

import Control.Concurrent.Async (mapConcurrently)
import Control.Monad (filterM, join)
import GHC.IO.Exception (ExitCode(ExitSuccess))
import System.Directory (doesDirectoryExist, getHomeDirectory, listDirectory)
import System.Process (CreateProcess(cwd), createProcess, shell, waitForProcess)

-- config, relative to user home directory
dirs :: [FilePath]
dirs = ["abs/aur", "friedbrice", "lumihq"]

-- Concat paths without fear
(+/) :: FilePath -> FilePath -> FilePath
(+/) "" "" = ""
(+/) parent child = case (last parent, head child) of
    ('/', '/') -> parent ++ tail child
    ('/', _) -> parent ++ child
    (_, '/') -> parent ++ child
    _ -> parent ++ "/" ++ child

fetchRepo :: FilePath -> CreateProcess
fetchRepo dir = (shell "git fetch --prune --all") { cwd = Just dir }

listRepos :: FilePath -> IO [FilePath]
listRepos parentdir = do
    files <- listDirectory parentdir
    let paths = (parentdir +/) <$> files
    filterM (doesDirectoryExist . (+/ ".git")) paths

concurrentlyRetryForever :: [CreateProcess] -> IO ()
concurrentlyRetryForever procs = do
    handles <- mapConcurrently createProcess procs
    codes <- traverse (waitForProcess . \(_,_,_,h) -> h) $ handles
    let failures = [ p | (p, c) <- zip procs codes, c /= ExitSuccess ]
    if null failures then pure () else concurrentlyRetryForever failures

main :: IO ()
main = do
    home <- getHomeDirectory
    let fullPaths = (home +/) <$> dirs
    repos <- join <$> traverse listRepos fullPaths
    concurrentlyRetryForever (fetchRepo <$> repos)
@chris-martin
Copy link
Member

Yes! This will be a great practical followup to threads.

I wonder if we could introduce a library or two to help condense this into a smaller example. Maybe path and path-io would take care of some of the path manipulation and directory searching?

@friedbrice
Copy link
Contributor Author

Cool. I'll look into path and path-io and see if i can clean this up.

@chris-martin chris-martin added the new example This issue is about writing a new example program. label Oct 14, 2019
@friedbrice
Copy link
Contributor Author

friedbrice commented Nov 4, 2019

How's this?

List Git repositories:

{-# LANGUAGE QuasiQuotes #-}

module ListRepos (listRepos, paths) where

import Control.Monad (filterM, join)
import Data.Foldable (traverse_)
import Path (Path, Abs, Rel, Dir, reldir, (</>))
import Path.IO (doesDirExist, getHomeDir, listDir)

paths :: [Path Rel Dir]
paths = [ [reldir|abs/aur|]
        , [reldir|friedbrice|]
        , [reldir|lumi-tech|]
        ]

isGitRepo :: Path Abs Dir -> IO Bool
isGitRepo dir = doesDirExist (dir </> [reldir|.git|])

listRepos :: Path Abs Dir -> IO [Path Abs Dir]
listRepos parentdir = do
    (subdirs, _) <- listDir parentdir
    filterM isGitRepo subdirs

main :: IO ()
main = do
    home <- getHomeDir
    let fullPaths = map (home </>) paths
    repos <- fmap join (traverse listRepos fullPaths)
    traverse_ print repos

Concurrently fetch Git repositories:

module FetchRepos where

import Control.Concurrent.Async (mapConcurrently)
import Control.Monad (join)
import Path (Path, Abs, Dir, toFilePath, (</>))
import Path.IO (getHomeDir)
import System.Exit (ExitCode(ExitSuccess))
import System.Process ( CreateProcess(cwd)
                      , createProcess
                      , shell
                      , waitForProcess
                      )

import ListRepos (listRepos, paths)

fetchRepo :: Path Abs Dir -> CreateProcess
fetchRepo dir =
    (shell "git fetch --prune --all")
    { cwd = Just (toFilePath dir) }

concurrentlyRetryForever :: [CreateProcess] -> IO ()
concurrentlyRetryForever procs = do
    handles <- mapConcurrently createProcess procs

    exitCodes <-
        traverse (waitForProcess . \(_,_,_,h) -> h) handles

    let failures = [ proc
                   | (proc, exitCode) <- zip procs exitCodes
                   , exitCode /= ExitSuccess
                   ]

    if (null failures) then pure ()
    else concurrentlyRetryForever failures

main :: IO ()
main = do
    home <- getHomeDir
    let fullPaths = map (home </>) paths
    repos <- fmap join (traverse listRepos fullPaths)
    concurrentlyRetryForever (map fetchRepo repos)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
new example This issue is about writing a new example program.
Projects
None yet
Development

No branches or pull requests

2 participants