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:
parent
5a5acb2d1c
commit
2cf38e4c2e
|
@ -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
|
||||
|
||||
|
|
2
Makefile
2
Makefile
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 )
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 )
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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 )
|
||||
|
||||
|
|
|
@ -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 )
|
||||
|
|
|
@ -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) )
|
||||
|
|
|
@ -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>
|
|
@ -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
|
|
@ -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)
|
||||
|
|
@ -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.
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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.
|
|
@ -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
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue