Fix external web url (#836)

Infer path from Navigation.Location

Build uses template, local dev uses elm-reactor

Remove unneeded local dev go server

Add script.js make target

Compiles and uglifies script.js

Before:
~570kb

After:
~170kb

Bootstrap loading state

Add trailing slash via JS & add routePrefix console param

Add Javascript script tag to `index.html` which adds a trailing slash to
the url pathname if none is present. This is done to ensure assets like
`script.js` are loaded properly.

Example without patch:
If the pathname is "mxinden.com/alertmanager" the browser will try to
download the `script.js` asset from "mxinden.com/script.js". This
request will fail.

Example with patch:
If the pathname is "mxinden.com/alertmanager", Javascript redirects the
browser to "mxinden.com/alertmanager/" and then the `script.js` asset
will be downloaded from "mxinden.com/alertmanager/script.js". This
request will succeed.

Add `-web.route-prefix` as a console parameter. This configures a
Prefix for the internal routes of web endpoints. Defaults to path of
-web.external-url like in *Prometheus*.

Trim slashes off of route prefix and add one slash at the beginning.
Make sure route prefix is not empty or just a slash before prefixing
router.
This commit is contained in:
stuart nelson 2017-06-07 22:38:39 +02:00 committed by Max Inden
parent 5a5acb2d1c
commit 2cf38e4c2e
36 changed files with 265 additions and 2174 deletions

View File

@ -14,7 +14,7 @@ script:
- make
- docker run --rm -t -v "$(pwd):/app" -w /app/ui/app elm-env elm-format --validate src/
- docker run --rm -t -v "$(pwd):/app" -w /app/ui/app elm-env elm-make --yes src/Main.elm --output script.js
- docker run --rm -t -v "$(pwd):/app" -w /app/ui/app elm-env make script.js
- docker run --rm -t -v "$(pwd):/usr/src/app" -w /usr/src/app golang make assets
- git diff --exit-code

View File

@ -60,7 +60,7 @@ assets:
-@$(GO) get -u github.com/jteeuwen/go-bindata/...
# Using "-mode 420" and "-modtime 1" to make assets make target deterministic.
# It sets all file permissions and time stamps to 420 and 1
@go-bindata $(bindata_flags) -mode 420 -modtime 1 -pkg ui -o ui/bindata.go ui/app/index.html ui/app/script.js ui/app/favicon.ico
@go-bindata $(bindata_flags) -mode 420 -modtime 1 -pkg ui -o ui/bindata.go ui/app/script.js ui/app/index.html ui/app/favicon.ico
@go-bindata $(bindata_flags) -mode 420 -modtime 1 -pkg deftmpl -o template/internal/deftmpl/bindata.go template/default.tmpl
promu:

View File

@ -23,7 +23,6 @@ import (
"net/url"
"os"
"os/signal"
"path"
"path/filepath"
"sort"
"strconv"
@ -83,6 +82,7 @@ func main() {
retention = flag.Duration("data.retention", 5*24*time.Hour, "How long to keep data for.")
externalURL = flag.String("web.external-url", "", "The URL under which Alertmanager is externally reachable (for example, if Alertmanager is served via a reverse proxy). Used for generating relative and absolute links back to Alertmanager itself. If the URL has a path portion, it will be used to prefix all HTTP endpoints served by Alertmanager. If omitted, relevant URL components will be derived automatically.")
routePrefix = flag.String("web.route-prefix", "", "Prefix for the internal routes of web endpoints. Defaults to path of -web.external-url.")
listenAddress = flag.String("web.listen-address", ":9093", "Address to listen on for the web interface and API.")
meshListen = flag.String("mesh.listen-address", net.JoinHostPort("0.0.0.0", strconv.Itoa(mesh.Port)), "mesh listen address. Pass an empty string to disable.")
@ -286,11 +286,24 @@ func main() {
os.Exit(1)
}
// Make routePrefix default to externalURL path if empty string.
if routePrefix == nil || *routePrefix == "" {
*routePrefix = amURL.Path
}
*routePrefix = "/" + strings.Trim(*routePrefix, "/")
router := route.New()
if *routePrefix != "/" {
router = router.WithPrefix(*routePrefix)
}
webReload := make(chan struct{})
ui.Register(router.WithPrefix(amURL.Path), webReload)
apiv.Register(router.WithPrefix(path.Join(amURL.Path, "/api")))
ui.Register(router, webReload)
apiv.Register(router.WithPrefix("/api"))
log.Infoln("Listening on", *listenAddress)
go listen(*listenAddress, router)

View File

@ -1,3 +1,3 @@
FROM node:6.10
RUN npm install -g elm@0.18.0 elm-format@0.6.1-alpha elm-test@0.18.3
RUN npm install -g elm@0.18.0 elm-format@0.6.1-alpha elm-test@0.18.3 uglify-js@3.0.15

View File

@ -8,8 +8,6 @@ This document describes how to:
## Dev Environment Setup
- Go installed (https://golang.org/dl/)
- This repo is cloned into your `$GOPATH`
- Elm is [installed](https://guide.elm-lang.org/install.html#install)
- Your editor is [configured](https://guide.elm-lang.org/install.html#configure-your-editor)
- [elm-format](https://github.com/avh4/elm-format) is installed
@ -48,15 +46,15 @@ Once you've installed Elm, install the dependencies listed in
## Local development workflow
TODO: Add instructions for running against local AlertManager.
For now, the easiest way to get started is to point your front-end to a running
version of AlertManager. Update `baseUrl` in `src/Utils/Api.elm` with the
correct address. Don't commit this and send it in as part of a pull request!
At the top level of this repo, follow the HA AlertManager instructions. Compile
the binary, then run with `goreman`. Add example alerts with the file provided
in the HA example folder.
```
# cd ui/app
# go run main.go --port 5000 --debug
# elm-reactor -p <port>
```
Your app should be available at `http://localhost:5000`, and automatically recompiled on file change.
Your app should be available at `http://localhost:<port>`. Navigate to
`src/Main.elm`. Any changes to the file system are detected automatically,
triggering a recompile of the project.

View File

@ -6,4 +6,10 @@ test:
elm-test
dev-server:
go run main.go --debug
elm-reactor
TEMPFILE := $(shell mktemp /tmp/elm-XXXXXXXXXX.js)
script.js: $(ELM_FILES)
elm make src/Main.elm --yes --output $(TEMPFILE)
uglifyjs $(TEMPFILE) --compress unused --mangle --output $(@)
rm $(TEMPFILE)

View File

@ -8,8 +8,15 @@
<script src="https://use.fontawesome.com/b7508bb100.js"></script>
</head>
<body>
<!-- Your source after making -->
<script src="/script.js"></script>
<script>Elm.Main.embed(document.body)</script>
<script>
//If there is no trailing slash at the end of the path in the url,
// add one. This ensures assets like script.js` are loaded properly
if (window.location.pathname.endsWith('/') === false) {
window.location.pathname = window.location.pathname + '/';
console.log('added slash');
}
</script>
<script src="script.js"></script>
<script>Elm.Main.embed(document.body, { production: true })</script>
</body>
</html>

View File

@ -1,63 +0,0 @@
package main
import (
"context"
"flag"
"log"
"net/http"
"os"
"os/exec"
"time"
"github.com/stuartnelson3/guac"
)
func main() {
var (
port = flag.String("port", "8080", "port to listen on")
dev = flag.Bool("dev", true, "enable code rebuilding")
debug = flag.Bool("debug", false, "enable elm debugger")
)
flag.Parse()
http.HandleFunc("/script.js", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "script.js")
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "index.html")
})
http.HandleFunc("/api/v1/silences", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "silences.json")
})
if *dev {
// Recompile the elm code whenever a change is detected.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
elmMakeArgs := []string{"make", "src/Main.elm", "--yes", "--output", "script.js"}
if *debug {
elmMakeArgs = append(elmMakeArgs, "--debug")
}
recompileFn := func() error {
cmd := exec.Command("elm", elmMakeArgs...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
_, err := guac.NewWatcher(ctx, "./src", 50*time.Millisecond, recompileFn)
if err != nil {
log.Fatalf("error watching: %v", err)
}
}
log.Printf("starting listener on port %s", *port)
if err := http.ListenAndServe(":"+*port, nil); err != nil {
log.Fatal(err)
}
}

View File

@ -2,16 +2,16 @@ module Alerts.Api exposing (..)
import Alerts.Types exposing (Alert, RouteOpts, Block, AlertGroup)
import Json.Decode as Json exposing (..)
import Utils.Api exposing (baseUrl, iso8601Time)
import Utils.Api exposing (iso8601Time)
import Utils.Types exposing (ApiData)
import Utils.Filter exposing (Filter, generateQueryString)
fetchAlerts : Filter -> Cmd (ApiData (List Alert))
fetchAlerts filter =
fetchAlerts : String -> Filter -> Cmd (ApiData (List Alert))
fetchAlerts apiUrl filter =
let
url =
String.join "/" [ baseUrl, "alerts" ++ (generateQueryString filter) ]
String.join "/" [ apiUrl, "alerts" ++ (generateQueryString filter) ]
in
Utils.Api.send (Utils.Api.get url alertsDecoder)

View File

@ -25,11 +25,14 @@ import Views.AlertList.Types exposing (initAlertList)
import Views.SilenceList.Types exposing (initSilenceList)
import Views.SilenceView.Types exposing (initSilenceView)
import Updates exposing (update)
import Utils.Api as Api
import Utils.Types exposing (ApiData(Loading))
import Json.Decode as Json
main : Program Never Model Msg
main : Program Json.Value Model Msg
main =
Navigation.program urlUpdate
Navigation.programWithFlags urlUpdate
{ init = init
, update = update
, view = Views.view
@ -37,8 +40,8 @@ main =
}
init : Navigation.Location -> ( Model, Cmd Msg )
init location =
init : Json.Value -> Navigation.Location -> ( Model, Cmd Msg )
init flags location =
let
route =
Parsing.urlParser location
@ -53,8 +56,31 @@ init location =
_ ->
nullFilter
prod =
flags
|> Json.decodeValue (Json.field "production" Json.bool)
|> Result.withDefault False
apiUrl =
if prod then
Api.makeApiUrl location.pathname
else
Api.makeApiUrl "http://localhost:9093"
in
update (urlUpdate location) (Model initSilenceList initSilenceView initSilenceForm initAlertList route filter initStatusModel)
update (urlUpdate location)
(Model
initSilenceList
initSilenceView
initSilenceForm
initAlertList
route
filter
initStatusModel
location.pathname
apiUrl
Loading
)
urlUpdate : Navigation.Location -> Msg

View File

@ -4,37 +4,37 @@ import Http
import Silences.Types exposing (Silence)
import Utils.Types exposing (ApiData(..))
import Utils.Filter exposing (Filter)
import Utils.Api
import Silences.Decoders exposing (show, list, create, destroy)
import Silences.Encoders
import Utils.Api exposing (baseUrl)
import Utils.Filter exposing (generateQueryString)
getSilences : Filter -> (ApiData (List Silence) -> msg) -> Cmd msg
getSilences filter msg =
getSilences : String -> Filter -> (ApiData (List Silence) -> msg) -> Cmd msg
getSilences apiUrl filter msg =
let
url =
String.join "/" [ baseUrl, "silences" ++ (generateQueryString filter) ]
String.join "/" [ apiUrl, "silences" ++ (generateQueryString filter) ]
in
Utils.Api.send (Utils.Api.get url list)
|> Cmd.map msg
getSilence : String -> (ApiData Silence -> msg) -> Cmd msg
getSilence uuid msg =
getSilence : String -> String -> (ApiData Silence -> msg) -> Cmd msg
getSilence apiUrl uuid msg =
let
url =
String.join "/" [ baseUrl, "silence", uuid ]
String.join "/" [ apiUrl, "silence", uuid ]
in
Utils.Api.send (Utils.Api.get url show)
|> Cmd.map msg
create : Silence -> Cmd (ApiData String)
create silence =
create : String -> Silence -> Cmd (ApiData String)
create apiUrl silence =
let
url =
String.join "/" [ baseUrl, "silences" ]
String.join "/" [ apiUrl, "silences" ]
body =
Http.jsonBody <| Silences.Encoders.silence silence
@ -45,13 +45,13 @@ create silence =
(Utils.Api.post url body Silences.Decoders.create)
destroy : Silence -> (ApiData String -> msg) -> Cmd msg
destroy silence msg =
destroy : String -> Silence -> (ApiData String -> msg) -> Cmd msg
destroy apiUrl silence msg =
-- The incorrect route using "silences" receives a 405. The route seems to
-- be matching on /silences and ignoring the :sid, should be getting a 404.
let
url =
String.join "/" [ baseUrl, "silence", silence.id ]
String.join "/" [ apiUrl, "silence", silence.id ]
responseDecoder =
-- Silences.Encoders.silence silence

View File

@ -1,16 +1,16 @@
module Status.Api exposing (getStatus)
import Utils.Api exposing (baseUrl, send, get)
import Utils.Api exposing (send, get)
import Utils.Types exposing (ApiData)
import Status.Types exposing (StatusResponse, VersionInfo, MeshStatus, MeshPeer)
import Json.Decode exposing (Decoder, map2, string, field, at, list, int)
getStatus : (ApiData StatusResponse -> msg) -> Cmd msg
getStatus msg =
getStatus : String -> (ApiData StatusResponse -> msg) -> Cmd msg
getStatus apiUrl msg =
let
url =
String.join "/" [ baseUrl, "status" ]
String.join "/" [ apiUrl, "status" ]
request =
get url decodeStatusResponse

View File

@ -7,6 +7,7 @@ import Views.SilenceView.Types as SilenceView exposing (SilenceViewMsg)
import Views.SilenceForm.Types as SilenceForm exposing (SilenceFormMsg)
import Views.Status.Types exposing (StatusModel, StatusMsg)
import Utils.Filter exposing (Filter)
import Utils.Types exposing (ApiData)
type alias Model =
@ -17,6 +18,9 @@ type alias Model =
, route : Route
, filter : Filter
, status : StatusModel
, basePath : String
, apiUrl : String
, bootstrapCSS : ApiData String
}
@ -37,6 +41,7 @@ type Msg
| Noop
| RedirectAlerts
| UpdateFilter String
| BootstrapCSSLoaded (ApiData String)
type Route

View File

@ -23,7 +23,7 @@ import String exposing (trim)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
update msg ({ basePath, apiUrl } as model) =
case msg of
CreateSilenceFromAlert { labels } ->
let
@ -31,21 +31,21 @@ update msg model =
List.map (\( k, v ) -> Matcher False k v) labels
( silenceForm, cmd ) =
Views.SilenceForm.Updates.update (NewSilenceFromMatchers matchers) model.silenceForm
Views.SilenceForm.Updates.update (NewSilenceFromMatchers matchers) model.silenceForm basePath apiUrl
in
( { model | silenceForm = silenceForm }, Cmd.map MsgForSilenceForm cmd )
NavigateToAlerts filter ->
let
( alertList, cmd ) =
Views.AlertList.Updates.update FetchAlerts model.alertList filter
Views.AlertList.Updates.update FetchAlerts model.alertList filter apiUrl basePath
in
( { model | alertList = alertList, route = AlertsRoute filter, filter = filter }, cmd )
NavigateToSilenceList filter ->
let
( silenceList, cmd ) =
Views.SilenceList.Updates.update FetchSilences model.silenceList filter
Views.SilenceList.Updates.update FetchSilences model.silenceList filter basePath apiUrl
in
( { model | silenceList = silenceList, route = SilenceListRoute filter, filter = filter }
, Cmd.map MsgForSilenceList cmd
@ -57,7 +57,7 @@ update msg model =
NavigateToSilenceView silenceId ->
let
( silenceView, cmd ) =
Views.SilenceView.Updates.update (InitSilenceView silenceId) model.silenceView
Views.SilenceView.Updates.update (InitSilenceView silenceId) model.silenceView apiUrl
in
( { model | route = SilenceViewRoute silenceId, silenceView = silenceView }
, Cmd.map MsgForSilenceView cmd
@ -78,7 +78,7 @@ update msg model =
( { model | route = NotFoundRoute }, Cmd.none )
RedirectAlerts ->
( model, Navigation.newUrl "/#/alerts" )
( model, Navigation.newUrl (basePath ++ "#/alerts") )
UpdateFilter text ->
let
@ -97,32 +97,35 @@ update msg model =
( model, Cmd.none )
MsgForStatus msg ->
Views.Status.Updates.update msg model
Views.Status.Updates.update msg model apiUrl
MsgForAlertList msg ->
let
( alertList, cmd ) =
Views.AlertList.Updates.update msg model.alertList model.filter
Views.AlertList.Updates.update msg model.alertList model.filter apiUrl basePath
in
( { model | alertList = alertList }, cmd )
MsgForSilenceList msg ->
let
( silenceList, cmd ) =
Views.SilenceList.Updates.update msg model.silenceList model.filter
Views.SilenceList.Updates.update msg model.silenceList model.filter basePath apiUrl
in
( { model | silenceList = silenceList }, Cmd.map MsgForSilenceList cmd )
MsgForSilenceView msg ->
let
( silenceView, cmd ) =
Views.SilenceView.Updates.update msg model.silenceView
Views.SilenceView.Updates.update msg model.silenceView apiUrl
in
( { model | silenceView = silenceView }, Cmd.map MsgForSilenceView cmd )
MsgForSilenceForm msg ->
let
( silenceForm, cmd ) =
Views.SilenceForm.Updates.update msg model.silenceForm
Views.SilenceForm.Updates.update msg model.silenceForm basePath apiUrl
in
( { model | silenceForm = silenceForm }, Cmd.map MsgForSilenceForm cmd )
BootstrapCSSLoaded css ->
( { model | bootstrapCSS = css }, Cmd.none )

View File

@ -94,9 +94,16 @@ iso8601Time =
Json.string
baseUrl : String
baseUrl =
"/api/v1"
makeApiUrl : String -> String
makeApiUrl externalUrl =
let
url =
if String.endsWith "/" externalUrl then
String.dropRight 1 externalUrl
else
externalUrl
in
url ++ "/api/v1"
defaultTimeout : Time.Time
@ -108,7 +115,3 @@ defaultTimeout =
(|:) =
-- Taken from elm-community/json-extra
flip (Json.map2 (|>))
-- "http://localhost:9093/api/v1"

View File

@ -1,9 +1,12 @@
module Views exposing (..)
import Html exposing (Html, text, div)
import Html.Attributes exposing (class)
import Types exposing (Msg(MsgForSilenceForm, MsgForSilenceView), Model, Route(..))
import Html exposing (Html, node, text, div)
import Html.Attributes exposing (class, rel, href, src, style)
import Html.Events exposing (on)
import Json.Decode exposing (succeed)
import Types exposing (Msg(MsgForSilenceForm, MsgForSilenceView, BootstrapCSSLoaded), Model, Route(..))
import Utils.Views exposing (error, loading)
import Utils.Types exposing (ApiData(Failure, Success))
import Views.SilenceList.Views as SilenceList
import Views.SilenceForm.Views as SilenceForm
import Views.AlertList.Views as AlertList
@ -16,12 +19,46 @@ import Views.NavBar.Views exposing (navBar)
view : Model -> Html Msg
view model =
div []
[ navBar model.route
, div [ class "container pb-4" ]
[ currentView model ]
[ renderLink "https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"
, fontAwesome
, case model.bootstrapCSS of
Success _ ->
div []
[ navBar model.route
, div [ class "container pb-4" ] [ currentView model ]
]
Failure err ->
div []
[ div [ style [ ( "padding", "40px" ), ( "color", "red" ) ] ] [ text err ]
, navBar model.route
, div [ class "container pb-4" ] [ currentView model ]
]
_ ->
text ""
]
renderLink : String -> Html Msg
renderLink url =
node "link"
[ href url
, rel "stylesheet"
, on "load" (succeed (BootstrapCSSLoaded (Success url)))
, on "error" (succeed (BootstrapCSSLoaded (Failure ("Failed to load Bootstrap CSS from: " ++ url))))
]
[]
fontAwesome : Html msg
fontAwesome =
node "script"
[ src "https://use.fontawesome.com/b7508bb100.js"
]
[]
currentView : Model -> Html Msg
currentView model =
case model.route of

View File

@ -12,61 +12,65 @@ import Utils.Filter exposing (generateQueryString)
import Views.GroupBar.Updates as GroupBar
update : AlertListMsg -> Model -> Filter -> ( Model, Cmd Types.Msg )
update msg ({ groupBar, filterBar } as model) filter =
case msg of
AlertsFetched listOfAlerts ->
( { model
| alerts = listOfAlerts
, groupBar =
case listOfAlerts of
Success alerts ->
{ groupBar
| list =
List.concatMap .labels alerts
|> List.map Tuple.first
|> Set.fromList
}
update : AlertListMsg -> Model -> Filter -> String -> String -> ( Model, Cmd Types.Msg )
update msg ({ groupBar, filterBar } as model) filter apiUrl basePath =
let
alertsUrl =
basePath ++ "/#/alerts"
in
case msg of
AlertsFetched listOfAlerts ->
( { model
| alerts = listOfAlerts
, groupBar =
case listOfAlerts of
Success alerts ->
{ groupBar
| list =
List.concatMap .labels alerts
|> List.map Tuple.first
|> Set.fromList
}
_ ->
groupBar
}
, Cmd.none
)
FetchAlerts ->
let
newGroupBar =
GroupBar.setFields filter groupBar
newFilterBar =
FilterBar.setMatchers filter filterBar
in
( { model | alerts = Loading, filterBar = newFilterBar, groupBar = newGroupBar, activeId = Nothing }
, Api.fetchAlerts filter |> Cmd.map (AlertsFetched >> MsgForAlertList)
_ ->
groupBar
}
, Cmd.none
)
ToggleSilenced showSilenced ->
( model
, Navigation.newUrl ("/#/alerts" ++ generateQueryString { filter | showSilenced = Just showSilenced })
)
FetchAlerts ->
let
newGroupBar =
GroupBar.setFields filter groupBar
SetTab tab ->
( { model | tab = tab }, Cmd.none )
newFilterBar =
FilterBar.setMatchers filter filterBar
in
( { model | alerts = Loading, filterBar = newFilterBar, groupBar = newGroupBar, activeId = Nothing }
, Api.fetchAlerts apiUrl filter |> Cmd.map (AlertsFetched >> MsgForAlertList)
)
MsgForFilterBar msg ->
let
( newFilterBar, cmd ) =
FilterBar.update "/#/alerts" filter msg filterBar
in
( { model | filterBar = newFilterBar, tab = FilterTab }, Cmd.map (MsgForFilterBar >> MsgForAlertList) cmd )
ToggleSilenced showSilenced ->
( model
, Navigation.newUrl (alertsUrl ++ generateQueryString { filter | showSilenced = Just showSilenced })
)
MsgForGroupBar msg ->
let
( newGroupBar, cmd ) =
GroupBar.update "/#/alerts" filter msg groupBar
in
( { model | groupBar = newGroupBar }, Cmd.map (MsgForGroupBar >> MsgForAlertList) cmd )
SetTab tab ->
( { model | tab = tab }, Cmd.none )
SetActive maybeId ->
( { model | activeId = maybeId }, Cmd.none )
MsgForFilterBar msg ->
let
( newFilterBar, cmd ) =
FilterBar.update alertsUrl filter msg filterBar
in
( { model | filterBar = newFilterBar, tab = FilterTab }, Cmd.map (MsgForFilterBar >> MsgForAlertList) cmd )
MsgForGroupBar msg ->
let
( newGroupBar, cmd ) =
GroupBar.update alertsUrl filter msg groupBar
in
( { model | groupBar = newGroupBar }, Cmd.map (MsgForGroupBar >> MsgForAlertList) cmd )
SetActive maybeId ->
( { model | activeId = maybeId }, Cmd.none )

View File

@ -169,14 +169,14 @@ updateForm msg form =
{ form | matchers = matchers }
update : SilenceFormMsg -> Model -> ( Model, Cmd SilenceFormMsg )
update msg model =
update : SilenceFormMsg -> Model -> String -> String -> ( Model, Cmd SilenceFormMsg )
update msg model basePath apiUrl =
case msg of
CreateSilence ->
case toSilence model.form of
Just silence ->
( { model | silenceId = Loading }
, Silences.Api.create silence |> Cmd.map SilenceCreate
, Silences.Api.create apiUrl silence |> Cmd.map SilenceCreate
)
Nothing ->
@ -192,7 +192,7 @@ update msg model =
cmd =
case silenceId of
Success id ->
Navigation.newUrl ("/#/silences/" ++ id)
Navigation.newUrl (basePath ++ "#/silences/" ++ id)
_ ->
Cmd.none
@ -211,7 +211,7 @@ update msg model =
)
FetchSilence silenceId ->
( model, Silences.Api.getSilence silenceId SilenceFetch )
( model, Silences.Api.getSilence apiUrl silenceId SilenceFetch )
SilenceFetch (Success silence) ->
( { model | form = fromSilence silence }
@ -226,6 +226,7 @@ update msg model =
Just silence ->
( { model | alerts = Loading }
, Alerts.Api.fetchAlerts
apiUrl
{ nullFilter | text = Just (Utils.List.mjoin silence.matchers) }
|> Cmd.map AlertGroupsPreview
)

View File

@ -7,8 +7,8 @@ import Utils.Filter exposing (Filter, generateQueryString)
import Views.FilterBar.Updates as FilterBar
update : SilenceListMsg -> Model -> Filter -> ( Model, Cmd SilenceListMsg )
update msg model filter =
update : SilenceListMsg -> Model -> Filter -> String -> String -> ( Model, Cmd SilenceListMsg )
update msg model filter basePath apiUrl =
case msg of
SilencesFetch sils ->
( { model | silences = sils }, Cmd.none )
@ -18,20 +18,20 @@ update msg model filter =
| filterBar = FilterBar.setMatchers filter model.filterBar
, silences = Loading
}
, Api.getSilences filter SilencesFetch
, Api.getSilences apiUrl filter SilencesFetch
)
DestroySilence silence ->
-- TODO: "Deleted id: ID" growl
-- TODO: Check why POST isn't there but is accepted
( { model | silences = Loading }
, Api.destroy silence (always FetchSilences)
, Api.destroy apiUrl silence (always FetchSilences)
)
MsgForFilterBar msg ->
let
( filterBar, cmd ) =
FilterBar.update "/#/silences" filter msg model.filterBar
FilterBar.update (basePath ++ "/#/silences") filter msg model.filterBar
in
( { model | filterBar = filterBar }, Cmd.map MsgForFilterBar cmd )

View File

@ -8,11 +8,11 @@ import Utils.Types exposing (ApiData(..))
import Utils.Filter exposing (nullFilter)
update : SilenceViewMsg -> Model -> ( Model, Cmd SilenceViewMsg )
update msg model =
update : SilenceViewMsg -> Model -> String -> ( Model, Cmd SilenceViewMsg )
update msg model basePath =
case msg of
FetchSilence id ->
( model, getSilence id SilenceFetched )
( model, getSilence basePath id SilenceFetched )
AlertGroupsPreview alerts ->
( { model | alerts = alerts }
@ -25,6 +25,7 @@ update msg model =
, alerts = Loading
}
, Alerts.Api.fetchAlerts
basePath
({ nullFilter | text = Just (Utils.List.mjoin silence.matchers), showSilenced = Just True })
|> Cmd.map AlertGroupsPreview
)
@ -33,4 +34,4 @@ update msg model =
( { model | silence = silence, alerts = Initial }, Cmd.none )
InitSilenceView silenceId ->
( model, getSilence silenceId SilenceFetched )
( model, getSilence basePath silenceId SilenceFetched )

View File

@ -5,11 +5,11 @@ import Views.Status.Types exposing (StatusMsg(..))
import Status.Api exposing (getStatus)
update : StatusMsg -> Model -> ( Model, Cmd Msg )
update msg model =
update : StatusMsg -> Model -> String -> ( Model, Cmd Msg )
update msg model basePath =
case msg of
NewStatus apiResponse ->
( { model | status = { statusInfo = apiResponse } }, Cmd.none )
InitStatusView ->
( model, getStatus (NewStatus >> MsgForStatus) )
( model, getStatus basePath (NewStatus >> MsgForStatus) )

View File

@ -1,28 +0,0 @@
# Names should be added to this file as
# Name or Organization <email address>
# The email address is not required for organizations.
# You can update this list using the following command:
#
# $ git shortlog -se | awk '{print $2 " " $3 " " $4}'
# Please keep the list sorted.
Adrien Bustany <adrien@bustany.org>
Caleb Spare <cespare@gmail.com>
Case Nelson <case@teammating.com>
Chris Howey <howeyc@gmail.com> <chris@howey.me>
Christoffer Buchholz <christoffer.buchholz@gmail.com>
Dave Cheney <dave@cheney.net>
Francisco Souza <f@souza.cc>
John C Barstow
Kelvin Fo <vmirage@gmail.com>
Nathan Youngman <git@nathany.com>
Paul Hammond <paul@paulhammond.org>
Pursuit92 <JoshChase@techpursuit.net>
Rob Figueiredo <robfig@gmail.com>
Travis Cline <travis.cline@gmail.com>
Tudor Golubenco <tudor.g@gmail.com>
bronze1man <bronze1man@gmail.com>
debrando <denis.brandolini@gmail.com>
henrikedwards <henrik.edwards@gmail.com>

View File

@ -1,160 +0,0 @@
# Changelog
## v0.9.0 / 2014-01-17
* IsAttrib() for events that only concern a file's metadata [#79][] (thanks @abustany)
* [Fix] kqueue: fix deadlock [#77][] (thanks @cespare)
* [NOTICE] Development has moved to `code.google.com/p/go.exp/fsnotify` in preparation for inclusion in the Go standard library.
## v0.8.12 / 2013-11-13
* [API] Remove FD_SET and friends from Linux adapter
## v0.8.11 / 2013-11-02
* [Doc] Add Changelog [#72][] (thanks @nathany)
* [Doc] Spotlight and double modify events on OS X [#62][] (reported by @paulhammond)
## v0.8.10 / 2013-10-19
* [Fix] kqueue: remove file watches when parent directory is removed [#71][] (reported by @mdwhatcott)
* [Fix] kqueue: race between Close and readEvents [#70][] (reported by @bernerdschaefer)
* [Doc] specify OS-specific limits in README (thanks @debrando)
## v0.8.9 / 2013-09-08
* [Doc] Contributing (thanks @nathany)
* [Doc] update package path in example code [#63][] (thanks @paulhammond)
* [Doc] GoCI badge in README (Linux only) [#60][]
* [Doc] Cross-platform testing with Vagrant [#59][] (thanks @nathany)
## v0.8.8 / 2013-06-17
* [Fix] Windows: handle `ERROR_MORE_DATA` on Windows [#49][] (thanks @jbowtie)
## v0.8.7 / 2013-06-03
* [API] Make syscall flags internal
* [Fix] inotify: ignore event changes
* [Fix] race in symlink test [#45][] (reported by @srid)
* [Fix] tests on Windows
* lower case error messages
## v0.8.6 / 2013-05-23
* kqueue: Use EVT_ONLY flag on Darwin
* [Doc] Update README with full example
## v0.8.5 / 2013-05-09
* [Fix] inotify: allow monitoring of "broken" symlinks (thanks @tsg)
## v0.8.4 / 2013-04-07
* [Fix] kqueue: watch all file events [#40][] (thanks @ChrisBuchholz)
## v0.8.3 / 2013-03-13
* [Fix] inoitfy/kqueue memory leak [#36][] (reported by @nbkolchin)
* [Fix] kqueue: use fsnFlags for watching a directory [#33][] (reported by @nbkolchin)
## v0.8.2 / 2013-02-07
* [Doc] add Authors
* [Fix] fix data races for map access [#29][] (thanks @fsouza)
## v0.8.1 / 2013-01-09
* [Fix] Windows path separators
* [Doc] BSD License
## v0.8.0 / 2012-11-09
* kqueue: directory watching improvements (thanks @vmirage)
* inotify: add `IN_MOVED_TO` [#25][] (requested by @cpisto)
* [Fix] kqueue: deleting watched directory [#24][] (reported by @jakerr)
## v0.7.4 / 2012-10-09
* [Fix] inotify: fixes from https://codereview.appspot.com/5418045/ (ugorji)
* [Fix] kqueue: preserve watch flags when watching for delete [#21][] (reported by @robfig)
* [Fix] kqueue: watch the directory even if it isn't a new watch (thanks @robfig)
* [Fix] kqueue: modify after recreation of file
## v0.7.3 / 2012-09-27
* [Fix] kqueue: watch with an existing folder inside the watched folder (thanks @vmirage)
* [Fix] kqueue: no longer get duplicate CREATE events
## v0.7.2 / 2012-09-01
* kqueue: events for created directories
## v0.7.1 / 2012-07-14
* [Fix] for renaming files
## v0.7.0 / 2012-07-02
* [Feature] FSNotify flags
* [Fix] inotify: Added file name back to event path
## v0.6.0 / 2012-06-06
* kqueue: watch files after directory created (thanks @tmc)
## v0.5.1 / 2012-05-22
* [Fix] inotify: remove all watches before Close()
## v0.5.0 / 2012-05-03
* [API] kqueue: return errors during watch instead of sending over channel
* kqueue: match symlink behavior on Linux
* inotify: add `DELETE_SELF` (requested by @taralx)
* [Fix] kqueue: handle EINTR (reported by @robfig)
* [Doc] Godoc example [#1][] (thanks @davecheney)
## v0.4.0 / 2012-03-30
* Go 1 released: build with go tool
* [Feature] Windows support using winfsnotify
* Windows does not have attribute change notifications
* Roll attribute notifications into IsModify
## v0.3.0 / 2012-02-19
* kqueue: add files when watch directory
## v0.2.0 / 2011-12-30
* update to latest Go weekly code
## v0.1.0 / 2011-10-19
* kqueue: add watch on file creation to match inotify
* kqueue: create file event
* inotify: ignore `IN_IGNORED` events
* event String()
* linux: common FileEvent functions
* initial commit
[#79]: https://github.com/howeyc/fsnotify/pull/79
[#77]: https://github.com/howeyc/fsnotify/pull/77
[#72]: https://github.com/howeyc/fsnotify/issues/72
[#71]: https://github.com/howeyc/fsnotify/issues/71
[#70]: https://github.com/howeyc/fsnotify/issues/70
[#63]: https://github.com/howeyc/fsnotify/issues/63
[#62]: https://github.com/howeyc/fsnotify/issues/62
[#60]: https://github.com/howeyc/fsnotify/issues/60
[#59]: https://github.com/howeyc/fsnotify/issues/59
[#49]: https://github.com/howeyc/fsnotify/issues/49
[#45]: https://github.com/howeyc/fsnotify/issues/45
[#40]: https://github.com/howeyc/fsnotify/issues/40
[#36]: https://github.com/howeyc/fsnotify/issues/36
[#33]: https://github.com/howeyc/fsnotify/issues/33
[#29]: https://github.com/howeyc/fsnotify/issues/29
[#25]: https://github.com/howeyc/fsnotify/issues/25
[#24]: https://github.com/howeyc/fsnotify/issues/24
[#21]: https://github.com/howeyc/fsnotify/issues/21
[#1]: https://github.com/howeyc/fsnotify/issues/1

View File

@ -1,7 +0,0 @@
# Contributing
## Moving Notice
There is a fork being actively developed with a new API in preparation for the Go Standard Library:
[github.com/go-fsnotify/fsnotify](https://github.com/go-fsnotify/fsnotify)

View File

@ -1,28 +0,0 @@
Copyright (c) 2012 The Go Authors. All rights reserved.
Copyright (c) 2012 fsnotify Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,93 +0,0 @@
# File system notifications for Go
[![GoDoc](https://godoc.org/github.com/howeyc/fsnotify?status.png)](http://godoc.org/github.com/howeyc/fsnotify)
Cross platform: Windows, Linux, BSD and OS X.
## Moving Notice
There is a fork being actively developed with a new API in preparation for the Go Standard Library:
[github.com/go-fsnotify/fsnotify](https://github.com/go-fsnotify/fsnotify)
## Example:
```go
package main
import (
"log"
"github.com/howeyc/fsnotify"
)
func main() {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
done := make(chan bool)
// Process events
go func() {
for {
select {
case ev := <-watcher.Event:
log.Println("event:", ev)
case err := <-watcher.Error:
log.Println("error:", err)
}
}
}()
err = watcher.Watch("testDir")
if err != nil {
log.Fatal(err)
}
// Hang so program doesn't exit
<-done
/* ... do stuff ... */
watcher.Close()
}
```
For each event:
* Name
* IsCreate()
* IsDelete()
* IsModify()
* IsRename()
## FAQ
**When a file is moved to another directory is it still being watched?**
No (it shouldn't be, unless you are watching where it was moved to).
**When I watch a directory, are all subdirectories watched as well?**
No, you must add watches for any directory you want to watch (a recursive watcher is in the works [#56][]).
**Do I have to watch the Error and Event channels in a separate goroutine?**
As of now, yes. Looking into making this single-thread friendly (see [#7][])
**Why am I receiving multiple events for the same file on OS X?**
Spotlight indexing on OS X can result in multiple events (see [#62][]). A temporary workaround is to add your folder(s) to the *Spotlight Privacy settings* until we have a native FSEvents implementation (see [#54][]).
**How many files can be watched at once?**
There are OS-specific limits as to how many watches can be created:
* Linux: /proc/sys/fs/inotify/max_user_watches contains the limit,
reaching this limit results in a "no space left on device" error.
* BSD / OSX: sysctl variables "kern.maxfiles" and "kern.maxfilesperproc", reaching these limits results in a "too many open files" error.
[#62]: https://github.com/howeyc/fsnotify/issues/62
[#56]: https://github.com/howeyc/fsnotify/issues/56
[#54]: https://github.com/howeyc/fsnotify/issues/54
[#7]: https://github.com/howeyc/fsnotify/issues/7

View File

@ -1,111 +0,0 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package fsnotify implements file system notification.
package fsnotify
import "fmt"
const (
FSN_CREATE = 1
FSN_MODIFY = 2
FSN_DELETE = 4
FSN_RENAME = 8
FSN_ALL = FSN_MODIFY | FSN_DELETE | FSN_RENAME | FSN_CREATE
)
// Purge events from interal chan to external chan if passes filter
func (w *Watcher) purgeEvents() {
for ev := range w.internalEvent {
sendEvent := false
w.fsnmut.Lock()
fsnFlags := w.fsnFlags[ev.Name]
w.fsnmut.Unlock()
if (fsnFlags&FSN_CREATE == FSN_CREATE) && ev.IsCreate() {
sendEvent = true
}
if (fsnFlags&FSN_MODIFY == FSN_MODIFY) && ev.IsModify() {
sendEvent = true
}
if (fsnFlags&FSN_DELETE == FSN_DELETE) && ev.IsDelete() {
sendEvent = true
}
if (fsnFlags&FSN_RENAME == FSN_RENAME) && ev.IsRename() {
sendEvent = true
}
if sendEvent {
w.Event <- ev
}
// If there's no file, then no more events for user
// BSD must keep watch for internal use (watches DELETEs to keep track
// what files exist for create events)
if ev.IsDelete() {
w.fsnmut.Lock()
delete(w.fsnFlags, ev.Name)
w.fsnmut.Unlock()
}
}
close(w.Event)
}
// Watch a given file path
func (w *Watcher) Watch(path string) error {
return w.WatchFlags(path, FSN_ALL)
}
// Watch a given file path for a particular set of notifications (FSN_MODIFY etc.)
func (w *Watcher) WatchFlags(path string, flags uint32) error {
w.fsnmut.Lock()
w.fsnFlags[path] = flags
w.fsnmut.Unlock()
return w.watch(path)
}
// Remove a watch on a file
func (w *Watcher) RemoveWatch(path string) error {
w.fsnmut.Lock()
delete(w.fsnFlags, path)
w.fsnmut.Unlock()
return w.removeWatch(path)
}
// String formats the event e in the form
// "filename: DELETE|MODIFY|..."
func (e *FileEvent) String() string {
var events string = ""
if e.IsCreate() {
events += "|" + "CREATE"
}
if e.IsDelete() {
events += "|" + "DELETE"
}
if e.IsModify() {
events += "|" + "MODIFY"
}
if e.IsRename() {
events += "|" + "RENAME"
}
if e.IsAttrib() {
events += "|" + "ATTRIB"
}
if len(events) > 0 {
events = events[1:]
}
return fmt.Sprintf("%q: %s", e.Name, events)
}

View File

@ -1,496 +0,0 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build freebsd openbsd netbsd dragonfly darwin
package fsnotify
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
"syscall"
)
const (
// Flags (from <sys/event.h>)
sys_NOTE_DELETE = 0x0001 /* vnode was removed */
sys_NOTE_WRITE = 0x0002 /* data contents changed */
sys_NOTE_EXTEND = 0x0004 /* size increased */
sys_NOTE_ATTRIB = 0x0008 /* attributes changed */
sys_NOTE_LINK = 0x0010 /* link count changed */
sys_NOTE_RENAME = 0x0020 /* vnode was renamed */
sys_NOTE_REVOKE = 0x0040 /* vnode access was revoked */
// Watch all events
sys_NOTE_ALLEVENTS = sys_NOTE_DELETE | sys_NOTE_WRITE | sys_NOTE_ATTRIB | sys_NOTE_RENAME
// Block for 100 ms on each call to kevent
keventWaitTime = 100e6
)
type FileEvent struct {
mask uint32 // Mask of events
Name string // File name (optional)
create bool // set by fsnotify package if found new file
}
// IsCreate reports whether the FileEvent was triggered by a creation
func (e *FileEvent) IsCreate() bool { return e.create }
// IsDelete reports whether the FileEvent was triggered by a delete
func (e *FileEvent) IsDelete() bool { return (e.mask & sys_NOTE_DELETE) == sys_NOTE_DELETE }
// IsModify reports whether the FileEvent was triggered by a file modification
func (e *FileEvent) IsModify() bool {
return ((e.mask&sys_NOTE_WRITE) == sys_NOTE_WRITE || (e.mask&sys_NOTE_ATTRIB) == sys_NOTE_ATTRIB)
}
// IsRename reports whether the FileEvent was triggered by a change name
func (e *FileEvent) IsRename() bool { return (e.mask & sys_NOTE_RENAME) == sys_NOTE_RENAME }
// IsAttrib reports whether the FileEvent was triggered by a change in the file metadata.
func (e *FileEvent) IsAttrib() bool {
return (e.mask & sys_NOTE_ATTRIB) == sys_NOTE_ATTRIB
}
type Watcher struct {
mu sync.Mutex // Mutex for the Watcher itself.
kq int // File descriptor (as returned by the kqueue() syscall)
watches map[string]int // Map of watched file descriptors (key: path)
wmut sync.Mutex // Protects access to watches.
fsnFlags map[string]uint32 // Map of watched files to flags used for filter
fsnmut sync.Mutex // Protects access to fsnFlags.
enFlags map[string]uint32 // Map of watched files to evfilt note flags used in kqueue
enmut sync.Mutex // Protects access to enFlags.
paths map[int]string // Map of watched paths (key: watch descriptor)
finfo map[int]os.FileInfo // Map of file information (isDir, isReg; key: watch descriptor)
pmut sync.Mutex // Protects access to paths and finfo.
fileExists map[string]bool // Keep track of if we know this file exists (to stop duplicate create events)
femut sync.Mutex // Protects access to fileExists.
externalWatches map[string]bool // Map of watches added by user of the library.
ewmut sync.Mutex // Protects access to externalWatches.
Error chan error // Errors are sent on this channel
internalEvent chan *FileEvent // Events are queued on this channel
Event chan *FileEvent // Events are returned on this channel
done chan bool // Channel for sending a "quit message" to the reader goroutine
isClosed bool // Set to true when Close() is first called
}
// NewWatcher creates and returns a new kevent instance using kqueue(2)
func NewWatcher() (*Watcher, error) {
fd, errno := syscall.Kqueue()
if fd == -1 {
return nil, os.NewSyscallError("kqueue", errno)
}
w := &Watcher{
kq: fd,
watches: make(map[string]int),
fsnFlags: make(map[string]uint32),
enFlags: make(map[string]uint32),
paths: make(map[int]string),
finfo: make(map[int]os.FileInfo),
fileExists: make(map[string]bool),
externalWatches: make(map[string]bool),
internalEvent: make(chan *FileEvent),
Event: make(chan *FileEvent),
Error: make(chan error),
done: make(chan bool, 1),
}
go w.readEvents()
go w.purgeEvents()
return w, nil
}
// Close closes a kevent watcher instance
// It sends a message to the reader goroutine to quit and removes all watches
// associated with the kevent instance
func (w *Watcher) Close() error {
w.mu.Lock()
if w.isClosed {
w.mu.Unlock()
return nil
}
w.isClosed = true
w.mu.Unlock()
// Send "quit" message to the reader goroutine
w.done <- true
w.wmut.Lock()
ws := w.watches
w.wmut.Unlock()
for path := range ws {
w.removeWatch(path)
}
return nil
}
// AddWatch adds path to the watched file set.
// The flags are interpreted as described in kevent(2).
func (w *Watcher) addWatch(path string, flags uint32) error {
w.mu.Lock()
if w.isClosed {
w.mu.Unlock()
return errors.New("kevent instance already closed")
}
w.mu.Unlock()
watchDir := false
w.wmut.Lock()
watchfd, found := w.watches[path]
w.wmut.Unlock()
if !found {
fi, errstat := os.Lstat(path)
if errstat != nil {
return errstat
}
// don't watch socket
if fi.Mode()&os.ModeSocket == os.ModeSocket {
return nil
}
// Follow Symlinks
// Unfortunately, Linux can add bogus symlinks to watch list without
// issue, and Windows can't do symlinks period (AFAIK). To maintain
// consistency, we will act like everything is fine. There will simply
// be no file events for broken symlinks.
// Hence the returns of nil on errors.
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
path, err := filepath.EvalSymlinks(path)
if err != nil {
return nil
}
fi, errstat = os.Lstat(path)
if errstat != nil {
return nil
}
}
fd, errno := syscall.Open(path, open_FLAGS, 0700)
if fd == -1 {
return errno
}
watchfd = fd
w.wmut.Lock()
w.watches[path] = watchfd
w.wmut.Unlock()
w.pmut.Lock()
w.paths[watchfd] = path
w.finfo[watchfd] = fi
w.pmut.Unlock()
}
// Watch the directory if it has not been watched before.
w.pmut.Lock()
w.enmut.Lock()
if w.finfo[watchfd].IsDir() &&
(flags&sys_NOTE_WRITE) == sys_NOTE_WRITE &&
(!found || (w.enFlags[path]&sys_NOTE_WRITE) != sys_NOTE_WRITE) {
watchDir = true
}
w.enmut.Unlock()
w.pmut.Unlock()
w.enmut.Lock()
w.enFlags[path] = flags
w.enmut.Unlock()
var kbuf [1]syscall.Kevent_t
watchEntry := &kbuf[0]
watchEntry.Fflags = flags
syscall.SetKevent(watchEntry, watchfd, syscall.EVFILT_VNODE, syscall.EV_ADD|syscall.EV_CLEAR)
entryFlags := watchEntry.Flags
success, errno := syscall.Kevent(w.kq, kbuf[:], nil, nil)
if success == -1 {
return errno
} else if (entryFlags & syscall.EV_ERROR) == syscall.EV_ERROR {
return errors.New("kevent add error")
}
if watchDir {
errdir := w.watchDirectoryFiles(path)
if errdir != nil {
return errdir
}
}
return nil
}
// Watch adds path to the watched file set, watching all events.
func (w *Watcher) watch(path string) error {
w.ewmut.Lock()
w.externalWatches[path] = true
w.ewmut.Unlock()
return w.addWatch(path, sys_NOTE_ALLEVENTS)
}
// RemoveWatch removes path from the watched file set.
func (w *Watcher) removeWatch(path string) error {
w.wmut.Lock()
watchfd, ok := w.watches[path]
w.wmut.Unlock()
if !ok {
return errors.New(fmt.Sprintf("can't remove non-existent kevent watch for: %s", path))
}
var kbuf [1]syscall.Kevent_t
watchEntry := &kbuf[0]
syscall.SetKevent(watchEntry, watchfd, syscall.EVFILT_VNODE, syscall.EV_DELETE)
entryFlags := watchEntry.Flags
success, errno := syscall.Kevent(w.kq, kbuf[:], nil, nil)
if success == -1 {
return os.NewSyscallError("kevent_rm_watch", errno)
} else if (entryFlags & syscall.EV_ERROR) == syscall.EV_ERROR {
return errors.New("kevent rm error")
}
syscall.Close(watchfd)
w.wmut.Lock()
delete(w.watches, path)
w.wmut.Unlock()
w.enmut.Lock()
delete(w.enFlags, path)
w.enmut.Unlock()
w.pmut.Lock()
delete(w.paths, watchfd)
fInfo := w.finfo[watchfd]
delete(w.finfo, watchfd)
w.pmut.Unlock()
// Find all watched paths that are in this directory that are not external.
if fInfo.IsDir() {
var pathsToRemove []string
w.pmut.Lock()
for _, wpath := range w.paths {
wdir, _ := filepath.Split(wpath)
if filepath.Clean(wdir) == filepath.Clean(path) {
w.ewmut.Lock()
if !w.externalWatches[wpath] {
pathsToRemove = append(pathsToRemove, wpath)
}
w.ewmut.Unlock()
}
}
w.pmut.Unlock()
for _, p := range pathsToRemove {
// Since these are internal, not much sense in propagating error
// to the user, as that will just confuse them with an error about
// a path they did not explicitly watch themselves.
w.removeWatch(p)
}
}
return nil
}
// readEvents reads from the kqueue file descriptor, converts the
// received events into Event objects and sends them via the Event channel
func (w *Watcher) readEvents() {
var (
eventbuf [10]syscall.Kevent_t // Event buffer
events []syscall.Kevent_t // Received events
twait *syscall.Timespec // Time to block waiting for events
n int // Number of events returned from kevent
errno error // Syscall errno
)
events = eventbuf[0:0]
twait = new(syscall.Timespec)
*twait = syscall.NsecToTimespec(keventWaitTime)
for {
// See if there is a message on the "done" channel
var done bool
select {
case done = <-w.done:
default:
}
// If "done" message is received
if done {
errno := syscall.Close(w.kq)
if errno != nil {
w.Error <- os.NewSyscallError("close", errno)
}
close(w.internalEvent)
close(w.Error)
return
}
// Get new events
if len(events) == 0 {
n, errno = syscall.Kevent(w.kq, nil, eventbuf[:], twait)
// EINTR is okay, basically the syscall was interrupted before
// timeout expired.
if errno != nil && errno != syscall.EINTR {
w.Error <- os.NewSyscallError("kevent", errno)
continue
}
// Received some events
if n > 0 {
events = eventbuf[0:n]
}
}
// Flush the events we received to the events channel
for len(events) > 0 {
fileEvent := new(FileEvent)
watchEvent := &events[0]
fileEvent.mask = uint32(watchEvent.Fflags)
w.pmut.Lock()
fileEvent.Name = w.paths[int(watchEvent.Ident)]
fileInfo := w.finfo[int(watchEvent.Ident)]
w.pmut.Unlock()
if fileInfo != nil && fileInfo.IsDir() && !fileEvent.IsDelete() {
// Double check to make sure the directory exist. This can happen when
// we do a rm -fr on a recursively watched folders and we receive a
// modification event first but the folder has been deleted and later
// receive the delete event
if _, err := os.Lstat(fileEvent.Name); os.IsNotExist(err) {
// mark is as delete event
fileEvent.mask |= sys_NOTE_DELETE
}
}
if fileInfo != nil && fileInfo.IsDir() && fileEvent.IsModify() && !fileEvent.IsDelete() {
w.sendDirectoryChangeEvents(fileEvent.Name)
} else {
// Send the event on the events channel
w.internalEvent <- fileEvent
}
// Move to next event
events = events[1:]
if fileEvent.IsRename() {
w.removeWatch(fileEvent.Name)
w.femut.Lock()
delete(w.fileExists, fileEvent.Name)
w.femut.Unlock()
}
if fileEvent.IsDelete() {
w.removeWatch(fileEvent.Name)
w.femut.Lock()
delete(w.fileExists, fileEvent.Name)
w.femut.Unlock()
// Look for a file that may have overwritten this
// (ie mv f1 f2 will delete f2 then create f2)
fileDir, _ := filepath.Split(fileEvent.Name)
fileDir = filepath.Clean(fileDir)
w.wmut.Lock()
_, found := w.watches[fileDir]
w.wmut.Unlock()
if found {
// make sure the directory exist before we watch for changes. When we
// do a recursive watch and perform rm -fr, the parent directory might
// have gone missing, ignore the missing directory and let the
// upcoming delete event remove the watch form the parent folder
if _, err := os.Lstat(fileDir); !os.IsNotExist(err) {
w.sendDirectoryChangeEvents(fileDir)
}
}
}
}
}
}
func (w *Watcher) watchDirectoryFiles(dirPath string) error {
// Get all files
files, err := ioutil.ReadDir(dirPath)
if err != nil {
return err
}
// Search for new files
for _, fileInfo := range files {
filePath := filepath.Join(dirPath, fileInfo.Name())
// Inherit fsnFlags from parent directory
w.fsnmut.Lock()
if flags, found := w.fsnFlags[dirPath]; found {
w.fsnFlags[filePath] = flags
} else {
w.fsnFlags[filePath] = FSN_ALL
}
w.fsnmut.Unlock()
if fileInfo.IsDir() == false {
// Watch file to mimic linux fsnotify
e := w.addWatch(filePath, sys_NOTE_ALLEVENTS)
if e != nil {
return e
}
} else {
// If the user is currently watching directory
// we want to preserve the flags used
w.enmut.Lock()
currFlags, found := w.enFlags[filePath]
w.enmut.Unlock()
var newFlags uint32 = sys_NOTE_DELETE
if found {
newFlags |= currFlags
}
// Linux gives deletes if not explicitly watching
e := w.addWatch(filePath, newFlags)
if e != nil {
return e
}
}
w.femut.Lock()
w.fileExists[filePath] = true
w.femut.Unlock()
}
return nil
}
// sendDirectoryEvents searches the directory for newly created files
// and sends them over the event channel. This functionality is to have
// the BSD version of fsnotify match linux fsnotify which provides a
// create event for files created in a watched directory.
func (w *Watcher) sendDirectoryChangeEvents(dirPath string) {
// Get all files
files, err := ioutil.ReadDir(dirPath)
if err != nil {
w.Error <- err
}
// Search for new files
for _, fileInfo := range files {
filePath := filepath.Join(dirPath, fileInfo.Name())
w.femut.Lock()
_, doesExist := w.fileExists[filePath]
w.femut.Unlock()
if !doesExist {
// Inherit fsnFlags from parent directory
w.fsnmut.Lock()
if flags, found := w.fsnFlags[dirPath]; found {
w.fsnFlags[filePath] = flags
} else {
w.fsnFlags[filePath] = FSN_ALL
}
w.fsnmut.Unlock()
// Send create event
fileEvent := new(FileEvent)
fileEvent.Name = filePath
fileEvent.create = true
w.internalEvent <- fileEvent
}
w.femut.Lock()
w.fileExists[filePath] = true
w.femut.Unlock()
}
w.watchDirectoryFiles(dirPath)
}

View File

@ -1,304 +0,0 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build linux
package fsnotify
import (
"errors"
"fmt"
"os"
"strings"
"sync"
"syscall"
"unsafe"
)
const (
// Options for inotify_init() are not exported
// sys_IN_CLOEXEC uint32 = syscall.IN_CLOEXEC
// sys_IN_NONBLOCK uint32 = syscall.IN_NONBLOCK
// Options for AddWatch
sys_IN_DONT_FOLLOW uint32 = syscall.IN_DONT_FOLLOW
sys_IN_ONESHOT uint32 = syscall.IN_ONESHOT
sys_IN_ONLYDIR uint32 = syscall.IN_ONLYDIR
// The "sys_IN_MASK_ADD" option is not exported, as AddWatch
// adds it automatically, if there is already a watch for the given path
// sys_IN_MASK_ADD uint32 = syscall.IN_MASK_ADD
// Events
sys_IN_ACCESS uint32 = syscall.IN_ACCESS
sys_IN_ALL_EVENTS uint32 = syscall.IN_ALL_EVENTS
sys_IN_ATTRIB uint32 = syscall.IN_ATTRIB
sys_IN_CLOSE uint32 = syscall.IN_CLOSE
sys_IN_CLOSE_NOWRITE uint32 = syscall.IN_CLOSE_NOWRITE
sys_IN_CLOSE_WRITE uint32 = syscall.IN_CLOSE_WRITE
sys_IN_CREATE uint32 = syscall.IN_CREATE
sys_IN_DELETE uint32 = syscall.IN_DELETE
sys_IN_DELETE_SELF uint32 = syscall.IN_DELETE_SELF
sys_IN_MODIFY uint32 = syscall.IN_MODIFY
sys_IN_MOVE uint32 = syscall.IN_MOVE
sys_IN_MOVED_FROM uint32 = syscall.IN_MOVED_FROM
sys_IN_MOVED_TO uint32 = syscall.IN_MOVED_TO
sys_IN_MOVE_SELF uint32 = syscall.IN_MOVE_SELF
sys_IN_OPEN uint32 = syscall.IN_OPEN
sys_AGNOSTIC_EVENTS = sys_IN_MOVED_TO | sys_IN_MOVED_FROM | sys_IN_CREATE | sys_IN_ATTRIB | sys_IN_MODIFY | sys_IN_MOVE_SELF | sys_IN_DELETE | sys_IN_DELETE_SELF
// Special events
sys_IN_ISDIR uint32 = syscall.IN_ISDIR
sys_IN_IGNORED uint32 = syscall.IN_IGNORED
sys_IN_Q_OVERFLOW uint32 = syscall.IN_Q_OVERFLOW
sys_IN_UNMOUNT uint32 = syscall.IN_UNMOUNT
)
type FileEvent struct {
mask uint32 // Mask of events
cookie uint32 // Unique cookie associating related events (for rename(2))
Name string // File name (optional)
}
// IsCreate reports whether the FileEvent was triggered by a creation
func (e *FileEvent) IsCreate() bool {
return (e.mask&sys_IN_CREATE) == sys_IN_CREATE || (e.mask&sys_IN_MOVED_TO) == sys_IN_MOVED_TO
}
// IsDelete reports whether the FileEvent was triggered by a delete
func (e *FileEvent) IsDelete() bool {
return (e.mask&sys_IN_DELETE_SELF) == sys_IN_DELETE_SELF || (e.mask&sys_IN_DELETE) == sys_IN_DELETE
}
// IsModify reports whether the FileEvent was triggered by a file modification or attribute change
func (e *FileEvent) IsModify() bool {
return ((e.mask&sys_IN_MODIFY) == sys_IN_MODIFY || (e.mask&sys_IN_ATTRIB) == sys_IN_ATTRIB)
}
// IsRename reports whether the FileEvent was triggered by a change name
func (e *FileEvent) IsRename() bool {
return ((e.mask&sys_IN_MOVE_SELF) == sys_IN_MOVE_SELF || (e.mask&sys_IN_MOVED_FROM) == sys_IN_MOVED_FROM)
}
// IsAttrib reports whether the FileEvent was triggered by a change in the file metadata.
func (e *FileEvent) IsAttrib() bool {
return (e.mask & sys_IN_ATTRIB) == sys_IN_ATTRIB
}
type watch struct {
wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
}
type Watcher struct {
mu sync.Mutex // Map access
fd int // File descriptor (as returned by the inotify_init() syscall)
watches map[string]*watch // Map of inotify watches (key: path)
fsnFlags map[string]uint32 // Map of watched files to flags used for filter
fsnmut sync.Mutex // Protects access to fsnFlags.
paths map[int]string // Map of watched paths (key: watch descriptor)
Error chan error // Errors are sent on this channel
internalEvent chan *FileEvent // Events are queued on this channel
Event chan *FileEvent // Events are returned on this channel
done chan bool // Channel for sending a "quit message" to the reader goroutine
isClosed bool // Set to true when Close() is first called
}
// NewWatcher creates and returns a new inotify instance using inotify_init(2)
func NewWatcher() (*Watcher, error) {
fd, errno := syscall.InotifyInit()
if fd == -1 {
return nil, os.NewSyscallError("inotify_init", errno)
}
w := &Watcher{
fd: fd,
watches: make(map[string]*watch),
fsnFlags: make(map[string]uint32),
paths: make(map[int]string),
internalEvent: make(chan *FileEvent),
Event: make(chan *FileEvent),
Error: make(chan error),
done: make(chan bool, 1),
}
go w.readEvents()
go w.purgeEvents()
return w, nil
}
// Close closes an inotify watcher instance
// It sends a message to the reader goroutine to quit and removes all watches
// associated with the inotify instance
func (w *Watcher) Close() error {
if w.isClosed {
return nil
}
w.isClosed = true
// Remove all watches
for path := range w.watches {
w.RemoveWatch(path)
}
// Send "quit" message to the reader goroutine
w.done <- true
return nil
}
// AddWatch adds path to the watched file set.
// The flags are interpreted as described in inotify_add_watch(2).
func (w *Watcher) addWatch(path string, flags uint32) error {
if w.isClosed {
return errors.New("inotify instance already closed")
}
w.mu.Lock()
watchEntry, found := w.watches[path]
w.mu.Unlock()
if found {
watchEntry.flags |= flags
flags |= syscall.IN_MASK_ADD
}
wd, errno := syscall.InotifyAddWatch(w.fd, path, flags)
if wd == -1 {
return errno
}
w.mu.Lock()
w.watches[path] = &watch{wd: uint32(wd), flags: flags}
w.paths[wd] = path
w.mu.Unlock()
return nil
}
// Watch adds path to the watched file set, watching all events.
func (w *Watcher) watch(path string) error {
return w.addWatch(path, sys_AGNOSTIC_EVENTS)
}
// RemoveWatch removes path from the watched file set.
func (w *Watcher) removeWatch(path string) error {
w.mu.Lock()
defer w.mu.Unlock()
watch, ok := w.watches[path]
if !ok {
return errors.New(fmt.Sprintf("can't remove non-existent inotify watch for: %s", path))
}
success, errno := syscall.InotifyRmWatch(w.fd, watch.wd)
if success == -1 {
return os.NewSyscallError("inotify_rm_watch", errno)
}
delete(w.watches, path)
return nil
}
// readEvents reads from the inotify file descriptor, converts the
// received events into Event objects and sends them via the Event channel
func (w *Watcher) readEvents() {
var (
buf [syscall.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events
n int // Number of bytes read with read()
errno error // Syscall errno
)
for {
// See if there is a message on the "done" channel
select {
case <-w.done:
syscall.Close(w.fd)
close(w.internalEvent)
close(w.Error)
return
default:
}
n, errno = syscall.Read(w.fd, buf[:])
// If EOF is received
if n == 0 {
syscall.Close(w.fd)
close(w.internalEvent)
close(w.Error)
return
}
if n < 0 {
w.Error <- os.NewSyscallError("read", errno)
continue
}
if n < syscall.SizeofInotifyEvent {
w.Error <- errors.New("inotify: short read in readEvents()")
continue
}
var offset uint32 = 0
// We don't know how many events we just read into the buffer
// While the offset points to at least one whole event...
for offset <= uint32(n-syscall.SizeofInotifyEvent) {
// Point "raw" to the event in the buffer
raw := (*syscall.InotifyEvent)(unsafe.Pointer(&buf[offset]))
event := new(FileEvent)
event.mask = uint32(raw.Mask)
event.cookie = uint32(raw.Cookie)
nameLen := uint32(raw.Len)
// If the event happened to the watched directory or the watched file, the kernel
// doesn't append the filename to the event, but we would like to always fill the
// the "Name" field with a valid filename. We retrieve the path of the watch from
// the "paths" map.
w.mu.Lock()
event.Name = w.paths[int(raw.Wd)]
w.mu.Unlock()
watchedName := event.Name
if nameLen > 0 {
// Point "bytes" at the first byte of the filename
bytes := (*[syscall.PathMax]byte)(unsafe.Pointer(&buf[offset+syscall.SizeofInotifyEvent]))
// The filename is padded with NUL bytes. TrimRight() gets rid of those.
event.Name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000")
}
// Send the events that are not ignored on the events channel
if !event.ignoreLinux() {
// Setup FSNotify flags (inherit from directory watch)
w.fsnmut.Lock()
if _, fsnFound := w.fsnFlags[event.Name]; !fsnFound {
if fsnFlags, watchFound := w.fsnFlags[watchedName]; watchFound {
w.fsnFlags[event.Name] = fsnFlags
} else {
w.fsnFlags[event.Name] = FSN_ALL
}
}
w.fsnmut.Unlock()
w.internalEvent <- event
}
// Move to the next event in the buffer
offset += syscall.SizeofInotifyEvent + nameLen
}
}
}
// Certain types of events can be "ignored" and not sent over the Event
// channel. Such as events marked ignore by the kernel, or MODIFY events
// against files that do not exist.
func (e *FileEvent) ignoreLinux() bool {
// Ignore anything the inotify API says to ignore
if e.mask&sys_IN_IGNORED == sys_IN_IGNORED {
return true
}
// If the event is not a DELETE or RENAME, the file must exist.
// Otherwise the event is ignored.
// *Note*: this was put in place because it was seen that a MODIFY
// event was sent after the DELETE. This ignores that MODIFY and
// assumes a DELETE will come or has come if the file doesn't exist.
if !(e.IsDelete() || e.IsRename()) {
_, statErr := os.Lstat(e.Name)
return os.IsNotExist(statErr)
}
return false
}

View File

@ -1,11 +0,0 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build freebsd openbsd netbsd dragonfly
package fsnotify
import "syscall"
const open_FLAGS = syscall.O_NONBLOCK | syscall.O_RDONLY

View File

@ -1,11 +0,0 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin
package fsnotify
import "syscall"
const open_FLAGS = syscall.O_EVTONLY

View File

@ -1,598 +0,0 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build windows
package fsnotify
import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"sync"
"syscall"
"unsafe"
)
const (
// Options for AddWatch
sys_FS_ONESHOT = 0x80000000
sys_FS_ONLYDIR = 0x1000000
// Events
sys_FS_ACCESS = 0x1
sys_FS_ALL_EVENTS = 0xfff
sys_FS_ATTRIB = 0x4
sys_FS_CLOSE = 0x18
sys_FS_CREATE = 0x100
sys_FS_DELETE = 0x200
sys_FS_DELETE_SELF = 0x400
sys_FS_MODIFY = 0x2
sys_FS_MOVE = 0xc0
sys_FS_MOVED_FROM = 0x40
sys_FS_MOVED_TO = 0x80
sys_FS_MOVE_SELF = 0x800
// Special events
sys_FS_IGNORED = 0x8000
sys_FS_Q_OVERFLOW = 0x4000
)
const (
// TODO(nj): Use syscall.ERROR_MORE_DATA from ztypes_windows in Go 1.3+
sys_ERROR_MORE_DATA syscall.Errno = 234
)
// Event is the type of the notification messages
// received on the watcher's Event channel.
type FileEvent struct {
mask uint32 // Mask of events
cookie uint32 // Unique cookie associating related events (for rename)
Name string // File name (optional)
}
// IsCreate reports whether the FileEvent was triggered by a creation
func (e *FileEvent) IsCreate() bool { return (e.mask & sys_FS_CREATE) == sys_FS_CREATE }
// IsDelete reports whether the FileEvent was triggered by a delete
func (e *FileEvent) IsDelete() bool {
return ((e.mask&sys_FS_DELETE) == sys_FS_DELETE || (e.mask&sys_FS_DELETE_SELF) == sys_FS_DELETE_SELF)
}
// IsModify reports whether the FileEvent was triggered by a file modification or attribute change
func (e *FileEvent) IsModify() bool {
return ((e.mask&sys_FS_MODIFY) == sys_FS_MODIFY || (e.mask&sys_FS_ATTRIB) == sys_FS_ATTRIB)
}
// IsRename reports whether the FileEvent was triggered by a change name
func (e *FileEvent) IsRename() bool {
return ((e.mask&sys_FS_MOVE) == sys_FS_MOVE || (e.mask&sys_FS_MOVE_SELF) == sys_FS_MOVE_SELF || (e.mask&sys_FS_MOVED_FROM) == sys_FS_MOVED_FROM || (e.mask&sys_FS_MOVED_TO) == sys_FS_MOVED_TO)
}
// IsAttrib reports whether the FileEvent was triggered by a change in the file metadata.
func (e *FileEvent) IsAttrib() bool {
return (e.mask & sys_FS_ATTRIB) == sys_FS_ATTRIB
}
const (
opAddWatch = iota
opRemoveWatch
)
const (
provisional uint64 = 1 << (32 + iota)
)
type input struct {
op int
path string
flags uint32
reply chan error
}
type inode struct {
handle syscall.Handle
volume uint32
index uint64
}
type watch struct {
ov syscall.Overlapped
ino *inode // i-number
path string // Directory path
mask uint64 // Directory itself is being watched with these notify flags
names map[string]uint64 // Map of names being watched and their notify flags
rename string // Remembers the old name while renaming a file
buf [4096]byte
}
type indexMap map[uint64]*watch
type watchMap map[uint32]indexMap
// A Watcher waits for and receives event notifications
// for a specific set of files and directories.
type Watcher struct {
mu sync.Mutex // Map access
port syscall.Handle // Handle to completion port
watches watchMap // Map of watches (key: i-number)
fsnFlags map[string]uint32 // Map of watched files to flags used for filter
fsnmut sync.Mutex // Protects access to fsnFlags.
input chan *input // Inputs to the reader are sent on this channel
internalEvent chan *FileEvent // Events are queued on this channel
Event chan *FileEvent // Events are returned on this channel
Error chan error // Errors are sent on this channel
isClosed bool // Set to true when Close() is first called
quit chan chan<- error
cookie uint32
}
// NewWatcher creates and returns a Watcher.
func NewWatcher() (*Watcher, error) {
port, e := syscall.CreateIoCompletionPort(syscall.InvalidHandle, 0, 0, 0)
if e != nil {
return nil, os.NewSyscallError("CreateIoCompletionPort", e)
}
w := &Watcher{
port: port,
watches: make(watchMap),
fsnFlags: make(map[string]uint32),
input: make(chan *input, 1),
Event: make(chan *FileEvent, 50),
internalEvent: make(chan *FileEvent),
Error: make(chan error),
quit: make(chan chan<- error, 1),
}
go w.readEvents()
go w.purgeEvents()
return w, nil
}
// Close closes a Watcher.
// It sends a message to the reader goroutine to quit and removes all watches
// associated with the watcher.
func (w *Watcher) Close() error {
if w.isClosed {
return nil
}
w.isClosed = true
// Send "quit" message to the reader goroutine
ch := make(chan error)
w.quit <- ch
if err := w.wakeupReader(); err != nil {
return err
}
return <-ch
}
// AddWatch adds path to the watched file set.
func (w *Watcher) AddWatch(path string, flags uint32) error {
if w.isClosed {
return errors.New("watcher already closed")
}
in := &input{
op: opAddWatch,
path: filepath.Clean(path),
flags: flags,
reply: make(chan error),
}
w.input <- in
if err := w.wakeupReader(); err != nil {
return err
}
return <-in.reply
}
// Watch adds path to the watched file set, watching all events.
func (w *Watcher) watch(path string) error {
return w.AddWatch(path, sys_FS_ALL_EVENTS)
}
// RemoveWatch removes path from the watched file set.
func (w *Watcher) removeWatch(path string) error {
in := &input{
op: opRemoveWatch,
path: filepath.Clean(path),
reply: make(chan error),
}
w.input <- in
if err := w.wakeupReader(); err != nil {
return err
}
return <-in.reply
}
func (w *Watcher) wakeupReader() error {
e := syscall.PostQueuedCompletionStatus(w.port, 0, 0, nil)
if e != nil {
return os.NewSyscallError("PostQueuedCompletionStatus", e)
}
return nil
}
func getDir(pathname string) (dir string, err error) {
attr, e := syscall.GetFileAttributes(syscall.StringToUTF16Ptr(pathname))
if e != nil {
return "", os.NewSyscallError("GetFileAttributes", e)
}
if attr&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
dir = pathname
} else {
dir, _ = filepath.Split(pathname)
dir = filepath.Clean(dir)
}
return
}
func getIno(path string) (ino *inode, err error) {
h, e := syscall.CreateFile(syscall.StringToUTF16Ptr(path),
syscall.FILE_LIST_DIRECTORY,
syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
nil, syscall.OPEN_EXISTING,
syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OVERLAPPED, 0)
if e != nil {
return nil, os.NewSyscallError("CreateFile", e)
}
var fi syscall.ByHandleFileInformation
if e = syscall.GetFileInformationByHandle(h, &fi); e != nil {
syscall.CloseHandle(h)
return nil, os.NewSyscallError("GetFileInformationByHandle", e)
}
ino = &inode{
handle: h,
volume: fi.VolumeSerialNumber,
index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow),
}
return ino, nil
}
// Must run within the I/O thread.
func (m watchMap) get(ino *inode) *watch {
if i := m[ino.volume]; i != nil {
return i[ino.index]
}
return nil
}
// Must run within the I/O thread.
func (m watchMap) set(ino *inode, watch *watch) {
i := m[ino.volume]
if i == nil {
i = make(indexMap)
m[ino.volume] = i
}
i[ino.index] = watch
}
// Must run within the I/O thread.
func (w *Watcher) addWatch(pathname string, flags uint64) error {
dir, err := getDir(pathname)
if err != nil {
return err
}
if flags&sys_FS_ONLYDIR != 0 && pathname != dir {
return nil
}
ino, err := getIno(dir)
if err != nil {
return err
}
w.mu.Lock()
watchEntry := w.watches.get(ino)
w.mu.Unlock()
if watchEntry == nil {
if _, e := syscall.CreateIoCompletionPort(ino.handle, w.port, 0, 0); e != nil {
syscall.CloseHandle(ino.handle)
return os.NewSyscallError("CreateIoCompletionPort", e)
}
watchEntry = &watch{
ino: ino,
path: dir,
names: make(map[string]uint64),
}
w.mu.Lock()
w.watches.set(ino, watchEntry)
w.mu.Unlock()
flags |= provisional
} else {
syscall.CloseHandle(ino.handle)
}
if pathname == dir {
watchEntry.mask |= flags
} else {
watchEntry.names[filepath.Base(pathname)] |= flags
}
if err = w.startRead(watchEntry); err != nil {
return err
}
if pathname == dir {
watchEntry.mask &= ^provisional
} else {
watchEntry.names[filepath.Base(pathname)] &= ^provisional
}
return nil
}
// Must run within the I/O thread.
func (w *Watcher) remWatch(pathname string) error {
dir, err := getDir(pathname)
if err != nil {
return err
}
ino, err := getIno(dir)
if err != nil {
return err
}
w.mu.Lock()
watch := w.watches.get(ino)
w.mu.Unlock()
if watch == nil {
return fmt.Errorf("can't remove non-existent watch for: %s", pathname)
}
if pathname == dir {
w.sendEvent(watch.path, watch.mask&sys_FS_IGNORED)
watch.mask = 0
} else {
name := filepath.Base(pathname)
w.sendEvent(watch.path+"\\"+name, watch.names[name]&sys_FS_IGNORED)
delete(watch.names, name)
}
return w.startRead(watch)
}
// Must run within the I/O thread.
func (w *Watcher) deleteWatch(watch *watch) {
for name, mask := range watch.names {
if mask&provisional == 0 {
w.sendEvent(watch.path+"\\"+name, mask&sys_FS_IGNORED)
}
delete(watch.names, name)
}
if watch.mask != 0 {
if watch.mask&provisional == 0 {
w.sendEvent(watch.path, watch.mask&sys_FS_IGNORED)
}
watch.mask = 0
}
}
// Must run within the I/O thread.
func (w *Watcher) startRead(watch *watch) error {
if e := syscall.CancelIo(watch.ino.handle); e != nil {
w.Error <- os.NewSyscallError("CancelIo", e)
w.deleteWatch(watch)
}
mask := toWindowsFlags(watch.mask)
for _, m := range watch.names {
mask |= toWindowsFlags(m)
}
if mask == 0 {
if e := syscall.CloseHandle(watch.ino.handle); e != nil {
w.Error <- os.NewSyscallError("CloseHandle", e)
}
w.mu.Lock()
delete(w.watches[watch.ino.volume], watch.ino.index)
w.mu.Unlock()
return nil
}
e := syscall.ReadDirectoryChanges(watch.ino.handle, &watch.buf[0],
uint32(unsafe.Sizeof(watch.buf)), false, mask, nil, &watch.ov, 0)
if e != nil {
err := os.NewSyscallError("ReadDirectoryChanges", e)
if e == syscall.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 {
// Watched directory was probably removed
if w.sendEvent(watch.path, watch.mask&sys_FS_DELETE_SELF) {
if watch.mask&sys_FS_ONESHOT != 0 {
watch.mask = 0
}
}
err = nil
}
w.deleteWatch(watch)
w.startRead(watch)
return err
}
return nil
}
// readEvents reads from the I/O completion port, converts the
// received events into Event objects and sends them via the Event channel.
// Entry point to the I/O thread.
func (w *Watcher) readEvents() {
var (
n, key uint32
ov *syscall.Overlapped
)
runtime.LockOSThread()
for {
e := syscall.GetQueuedCompletionStatus(w.port, &n, &key, &ov, syscall.INFINITE)
watch := (*watch)(unsafe.Pointer(ov))
if watch == nil {
select {
case ch := <-w.quit:
w.mu.Lock()
var indexes []indexMap
for _, index := range w.watches {
indexes = append(indexes, index)
}
w.mu.Unlock()
for _, index := range indexes {
for _, watch := range index {
w.deleteWatch(watch)
w.startRead(watch)
}
}
var err error
if e := syscall.CloseHandle(w.port); e != nil {
err = os.NewSyscallError("CloseHandle", e)
}
close(w.internalEvent)
close(w.Error)
ch <- err
return
case in := <-w.input:
switch in.op {
case opAddWatch:
in.reply <- w.addWatch(in.path, uint64(in.flags))
case opRemoveWatch:
in.reply <- w.remWatch(in.path)
}
default:
}
continue
}
switch e {
case sys_ERROR_MORE_DATA:
if watch == nil {
w.Error <- errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer")
} else {
// The i/o succeeded but the buffer is full.
// In theory we should be building up a full packet.
// In practice we can get away with just carrying on.
n = uint32(unsafe.Sizeof(watch.buf))
}
case syscall.ERROR_ACCESS_DENIED:
// Watched directory was probably removed
w.sendEvent(watch.path, watch.mask&sys_FS_DELETE_SELF)
w.deleteWatch(watch)
w.startRead(watch)
continue
case syscall.ERROR_OPERATION_ABORTED:
// CancelIo was called on this handle
continue
default:
w.Error <- os.NewSyscallError("GetQueuedCompletionPort", e)
continue
case nil:
}
var offset uint32
for {
if n == 0 {
w.internalEvent <- &FileEvent{mask: sys_FS_Q_OVERFLOW}
w.Error <- errors.New("short read in readEvents()")
break
}
// Point "raw" to the event in the buffer
raw := (*syscall.FileNotifyInformation)(unsafe.Pointer(&watch.buf[offset]))
buf := (*[syscall.MAX_PATH]uint16)(unsafe.Pointer(&raw.FileName))
name := syscall.UTF16ToString(buf[:raw.FileNameLength/2])
fullname := watch.path + "\\" + name
var mask uint64
switch raw.Action {
case syscall.FILE_ACTION_REMOVED:
mask = sys_FS_DELETE_SELF
case syscall.FILE_ACTION_MODIFIED:
mask = sys_FS_MODIFY
case syscall.FILE_ACTION_RENAMED_OLD_NAME:
watch.rename = name
case syscall.FILE_ACTION_RENAMED_NEW_NAME:
if watch.names[watch.rename] != 0 {
watch.names[name] |= watch.names[watch.rename]
delete(watch.names, watch.rename)
mask = sys_FS_MOVE_SELF
}
}
sendNameEvent := func() {
if w.sendEvent(fullname, watch.names[name]&mask) {
if watch.names[name]&sys_FS_ONESHOT != 0 {
delete(watch.names, name)
}
}
}
if raw.Action != syscall.FILE_ACTION_RENAMED_NEW_NAME {
sendNameEvent()
}
if raw.Action == syscall.FILE_ACTION_REMOVED {
w.sendEvent(fullname, watch.names[name]&sys_FS_IGNORED)
delete(watch.names, name)
}
if w.sendEvent(fullname, watch.mask&toFSnotifyFlags(raw.Action)) {
if watch.mask&sys_FS_ONESHOT != 0 {
watch.mask = 0
}
}
if raw.Action == syscall.FILE_ACTION_RENAMED_NEW_NAME {
fullname = watch.path + "\\" + watch.rename
sendNameEvent()
}
// Move to the next event in the buffer
if raw.NextEntryOffset == 0 {
break
}
offset += raw.NextEntryOffset
// Error!
if offset >= n {
w.Error <- errors.New("Windows system assumed buffer larger than it is, events have likely been missed.")
break
}
}
if err := w.startRead(watch); err != nil {
w.Error <- err
}
}
}
func (w *Watcher) sendEvent(name string, mask uint64) bool {
if mask == 0 {
return false
}
event := &FileEvent{mask: uint32(mask), Name: name}
if mask&sys_FS_MOVE != 0 {
if mask&sys_FS_MOVED_FROM != 0 {
w.cookie++
}
event.cookie = w.cookie
}
select {
case ch := <-w.quit:
w.quit <- ch
case w.Event <- event:
}
return true
}
func toWindowsFlags(mask uint64) uint32 {
var m uint32
if mask&sys_FS_ACCESS != 0 {
m |= syscall.FILE_NOTIFY_CHANGE_LAST_ACCESS
}
if mask&sys_FS_MODIFY != 0 {
m |= syscall.FILE_NOTIFY_CHANGE_LAST_WRITE
}
if mask&sys_FS_ATTRIB != 0 {
m |= syscall.FILE_NOTIFY_CHANGE_ATTRIBUTES
}
if mask&(sys_FS_MOVE|sys_FS_CREATE|sys_FS_DELETE) != 0 {
m |= syscall.FILE_NOTIFY_CHANGE_FILE_NAME | syscall.FILE_NOTIFY_CHANGE_DIR_NAME
}
return m
}
func toFSnotifyFlags(action uint32) uint64 {
switch action {
case syscall.FILE_ACTION_ADDED:
return sys_FS_CREATE
case syscall.FILE_ACTION_REMOVED:
return sys_FS_DELETE
case syscall.FILE_ACTION_MODIFIED:
return sys_FS_MODIFY
case syscall.FILE_ACTION_RENAMED_OLD_NAME:
return sys_FS_MOVED_FROM
case syscall.FILE_ACTION_RENAMED_NEW_NAME:
return sys_FS_MOVED_TO
}
return 0
}

View File

@ -1,11 +0,0 @@
guac
====
guac watches a directory tree for changes and executes a function whenever a file is added, removed, or modified.
## use
create a go program that imports guac and any of the guac packages you want (or your own arbitrary callback).
check out the examples folder for an example that concatenates all css and js files into single `application.css` and `application.js` files.

View File

@ -1,72 +0,0 @@
package guac
import (
"context"
"log"
"os"
"path/filepath"
"time"
"github.com/howeyc/fsnotify"
)
// Watch blocks until the the Watcher's context is canceled or its Done channel
// closed. It executes function fn when changes in srcDir are detected.
func (w *Watcher) run() {
filepath.Walk(w.srcDir, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
err = w.Watch(path)
if err != nil {
return err
}
log.Printf("Watching for file changes in %s\n", path)
}
return nil
})
defer w.Close()
for {
select {
case <-w.ctx.Done():
return
case <-w.Event:
w.debounce.Stop()
w.debounce = time.AfterFunc(w.debounceTime, func() { w.fn() })
case err := <-w.Error:
log.Println("error:", err)
}
}
}
// Watcher watches.
type Watcher struct {
ctx context.Context
srcDir string
fn func() error
debounceTime time.Duration
debounce *time.Timer
*fsnotify.Watcher
}
// NewWatcher creates a new watcher.
func NewWatcher(ctx context.Context, srcDir string, debounceTime time.Duration, fn func() error) (*Watcher, error) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
w := &Watcher{
ctx: ctx,
srcDir: srcDir,
fn: fn,
debounceTime: debounceTime,
Watcher: watcher,
}
w.debounce = time.AfterFunc(w.debounceTime, func() { w.fn() })
go w.run()
return w, nil
}

View File

@ -1,20 +0,0 @@
{
"comment": "",
"ignore": "test",
"package": [
{
"checksumSHA1": "ZxzYc1JwJ3U6kZbw/KGuPko5lSY=",
"origin": "github.com/stuartnelson3/guac/vendor/github.com/howeyc/fsnotify",
"path": "github.com/howeyc/fsnotify",
"revision": "e6a44e9b009f06370480facd69da9cc5401741e9",
"revisionTime": "2016-10-29T13:20:31Z"
},
{
"checksumSHA1": "Y7/vrMEQvLkfRJinNMxl5LyJArA=",
"path": "github.com/stuartnelson3/guac",
"revision": "63e17125d08d68a949a1ff3a56dceef4ff4540b6",
"revisionTime": "2017-03-14T10:10:04Z"
}
],
"rootPath": "github.com/stuartnelson3/am-ui"
}

File diff suppressed because one or more lines are too long