├── .gitignore ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── src └── main └── scala └── com └── megri └── sbt └── macos └── watcher ├── MacOSWatchService.scala └── MacOSWatchServicePlugin.scala /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | project/boot 3 | target/ 4 | .idea/ 5 | 6 | storage.db 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sbt-macos-watcher 2 | 3 | Hotfix for lethargic watchService on SBT 1.0+ MacOS. 4 | 5 | ## Usage 6 | 7 | Edit `~/.sbt/1.0/plugins/plugins.sbt` and add the following lines: 8 | 9 | ``` 10 | resolvers += Resolver.bintrayIvyRepo( "megri", "sbt-plugins" ) 11 | addSbtPlugin( "com.megri" % "sbt-macos-watcher" % "0.1" ) 12 | ``` 13 | 14 | The plugin should automatically replace the default sbt watchservice with a native one. 15 | 16 | ## Disclaimer 17 | 18 | Things may not work as intended for your use-case. Feel free to create an issue. 19 | 20 | ## Legal 21 | 22 | License is transitive on https://github.com/gjoseph/BarbaryWatchService, which borrows code from OpenJDK 7 "as much as possible". I'm no lawyer but I believe GPL 2.0-CE should work. If this is incorrect and you care about licensing, please add an issue so I can correct it. 23 | 24 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | sbtPlugin := true 2 | 3 | name := "sbt-macos-watcher" 4 | 5 | organization := "com.megri" 6 | 7 | version := "0.1" 8 | 9 | libraryDependencies += "net.incongru.watchservice" % "barbary-watchservice" % "1.0" 10 | 11 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.0.3 2 | 3 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.1") 2 | -------------------------------------------------------------------------------- /src/main/scala/com/megri/sbt/macos/watcher/MacOSWatchService.scala: -------------------------------------------------------------------------------- 1 | package com.megri.sbt.macos.watcher 2 | 3 | import java.io.{File => JFile} 4 | import java.nio.file.{Path => JPath, WatchEvent => JWatchEvent, WatchKey => JWatchKey, Watchable => JWatchable} 5 | import java.util.{List => JList} 6 | 7 | import com.barbarysoftware.watchservice.{WatchEvent => BWatchEvent, WatchKey => BWatchKey, WatchService => BWatchService, WatchableFile => BWatchableFile} 8 | 9 | import scala.collection.JavaConverters._ 10 | import scala.collection.mutable 11 | import scala.concurrent.duration._ 12 | 13 | class MacOSWatchService extends sbt.io.WatchService with BJConverters { 14 | import MacOSWatchServicePlugin.logger 15 | 16 | private[this] val underlying = BWatchService.newWatchService() 17 | private[this] val registered = mutable.Buffer.empty[JWatchKey] 18 | private[this] val name = getClass.getSimpleName 19 | 20 | override def init(): Unit = { 21 | logger.debug( s"$name: init()") 22 | logger.info( "Using native WatchService" ) 23 | } 24 | 25 | override def pollEvents(): Map[JWatchKey, Seq[JWatchEvent[JPath]]] = { 26 | logger.debug( s"$name: pollEvents()") 27 | registered.flatMap{ watchKey => 28 | val events = watchKey.pollEvents() 29 | 30 | if ( events == null ) None 31 | else Some( watchKey -> events.asScala.asInstanceOf[Seq[JWatchEvent[JPath]]] ) 32 | }.toMap 33 | } 34 | 35 | // This doesn't seem to get called; leaving it unimplemented for now 36 | override def poll( timeout: Duration ): JWatchKey = { 37 | logger.warn( s"$name: poll()") 38 | ??? 39 | } 40 | 41 | override def register( jPath: JPath, jEvents: JWatchEvent.Kind[JPath]* ): JWatchKey = { 42 | logger.debug( s"$name: register( $jPath, [${jEvents.mkString(", ")}])") 43 | val bwf = new BWatchableFile( jPath.toFile ) 44 | val bEvents = jEvents.map( e => convertJWatchEventKind( e ) ) 45 | val bKey = bwf.register( underlying, bEvents: _* ) 46 | val jKey = convertBWatchKey( jPath, bKey ) 47 | registered += jKey 48 | jKey 49 | } 50 | 51 | override def close(): Unit = { 52 | logger.debug( s"$name: close()") 53 | underlying.close() 54 | } 55 | } 56 | 57 | trait BJConverters { 58 | import java.nio.file.{StandardWatchEventKinds => JKind} 59 | import com.barbarysoftware.watchservice.{StandardWatchEventKind => BKind} 60 | 61 | def convertBWatchKey( jPath: JPath, bWatchKey: BWatchKey): JWatchKey = 62 | new JWatchKey { 63 | override def cancel( ): Unit = 64 | bWatchKey.cancel() 65 | 66 | override def pollEvents(): JList[JWatchEvent[_]] = 67 | bWatchKey.pollEvents().asScala 68 | .map( event => convertBWatchEvent( event ) ) 69 | .asJava 70 | 71 | override def watchable( ): JWatchable = jPath 72 | 73 | override def isValid: Boolean = 74 | bWatchKey.isValid 75 | 76 | override def reset( ): Boolean = 77 | bWatchKey.reset() 78 | } 79 | 80 | def convertBWatchEvent( bWatchEvent: BWatchEvent[_] ): JWatchEvent[_] = 81 | new JWatchEvent[JPath] { 82 | override def kind(): JWatchEvent.Kind[JPath] = bWatchEvent.kind match { 83 | case BKind.ENTRY_CREATE => JKind.ENTRY_CREATE 84 | case BKind.ENTRY_DELETE => JKind.ENTRY_DELETE 85 | case BKind.ENTRY_MODIFY => JKind.ENTRY_MODIFY 86 | // case BKind.OVERFLOW => JKind.OVERFLOW 87 | } 88 | 89 | override def count() = 90 | bWatchEvent.count() 91 | 92 | override def context(): JPath = 93 | bWatchEvent.context().asInstanceOf[JFile].toPath 94 | } 95 | 96 | def convertJWatchEventKind( jKind: JWatchEvent.Kind[_] ): BWatchEvent.Kind[_] = jKind match { 97 | case JKind.ENTRY_CREATE => BKind.ENTRY_CREATE 98 | case JKind.ENTRY_DELETE => BKind.ENTRY_DELETE 99 | case JKind.ENTRY_MODIFY => BKind.ENTRY_MODIFY 100 | case JKind.OVERFLOW => BKind.OVERFLOW 101 | } 102 | 103 | def convertJPath( jPath: JPath ): BWatchableFile = 104 | new BWatchableFile( jPath.toFile ) 105 | 106 | def convertBWatchableFile( bWatchableFile: BWatchableFile ): JPath = 107 | bWatchableFile.getFile.toPath 108 | } 109 | -------------------------------------------------------------------------------- /src/main/scala/com/megri/sbt/macos/watcher/MacOSWatchServicePlugin.scala: -------------------------------------------------------------------------------- 1 | package com.megri.sbt.macos.watcher 2 | 3 | import java.nio.file.FileSystems 4 | 5 | import sbt.Keys.watchService 6 | import sbt.Watched.PollDelay 7 | import sbt.io.PollingWatchService 8 | import sbt.{AutoPlugin, ConsoleLogger} 9 | 10 | import scala.util.Properties 11 | 12 | object MacOSWatchServicePlugin extends AutoPlugin { 13 | private[watcher] val logger = ConsoleLogger() 14 | 15 | lazy val watchServiceSetting = 16 | watchService := { () => 17 | if ( Properties.isMac ) 18 | new MacOSWatchService 19 | else 20 | sys.props.get( "sbt.watch.mode" ) match { 21 | case Some( "polling" ) => 22 | new PollingWatchService( PollDelay ) 23 | case _ => 24 | FileSystems.getDefault.newWatchService() 25 | } 26 | } 27 | 28 | override def requires = sbt.plugins.JvmPlugin 29 | override def trigger = allRequirements 30 | override def globalSettings = Seq( watchServiceSetting ) 31 | } 32 | --------------------------------------------------------------------------------