From bb34ab711770bac64805fbe47ea93b61dc85cf35 Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Wed, 4 Jan 2023 16:49:01 +0100 Subject: [PATCH 01/14] TempPath (<: AutoCloseable) and withTempFile | withTempDir convenience methods Convenience methods that creates a temporary file|dir, passes it them to a given function and remove them immediately after the function completed, even if the function threw an exception. This doesn't rely on the JVM's `deleteOnExit` handler, therefor the temp file gets deleted even if JVM never stopps gracefully (e.g. because it's intended to run continually, or the process get's killed from the outside). ```scala ---- withTempFile { file => os.write(file, "some content") } withTempDir { file => val file = dir / "somefile" os.write(file, "some content") } ``` implements https://github.com/com-lihaoyi/os-lib/discussions/140 --- Readme.adoc | 37 +++++++++++++++++ os/src/Path.scala | 5 +++ os/src/TempOps.scala | 59 ++++++++++++++++++++++---- os/test/src/TempPathTests.scala | 73 +++++++++++++++++++++++++++++++++ 4 files changed, 166 insertions(+), 8 deletions(-) create mode 100644 os/test/src/TempPathTests.scala diff --git a/Readme.adoc b/Readme.adoc index 4c04df23..7d2eca6f 100644 --- a/Readme.adoc +++ b/Readme.adoc @@ -1157,6 +1157,43 @@ os.followLink(wd / "misc" / "folder-symlink") ==> Some(wd / "folder1") os.followLink(wd / "misc" / "broken-symlink") ==> None ---- +==== `os.temp.withTempFile` + +[source,scala] +---- +os.temp.withTempFile[A](fun: Path => A): Try[A] +---- + +Creates a temporary file, passes it to the given function and removes it immediately after the function completed, even if the function threw an exception. + +This doesn't rely on the JVM's `deleteOnExit` handler, therefor the temp file gets deleted even if JVM never stopps gracefully (e.g. because it's intended to run continually, or the process get's killed from the outside). + +[source,scala] +---- +withTempFile { file => + os.write(file, "some content") +} +---- + +==== `os.temp.withTempDir` + +[source,scala] +---- +os.temp.withTempDir[A](fun: Path => A): Try[A] +---- + +Creates a temporary directory, passes it to the given function and removes it immediately after the function completed, even if the function threw an exception. + +This doesn't rely on the JVM's `deleteOnExit` handler, therefor the temp dir gets deleted even if JVM never stopps gracefully (e.g. because it's intended to run continually, or the process get's killed from the outside). + +[source,scala] +---- +withTempDir { file => + val file = dir / "somefile" + os.write(file, "some content") +} +---- + ==== `os.temp` [source,scala] diff --git a/os/src/Path.scala b/os/src/Path.scala index 019170eb..898f8080 100644 --- a/os/src/Path.scala +++ b/os/src/Path.scala @@ -498,6 +498,11 @@ class Path private[os] (val wrapped: java.nio.file.Path) def getInputStream = java.nio.file.Files.newInputStream(wrapped) } +class TempPath private[os] (wrapped: java.nio.file.Path) + extends Path(wrapped) with AutoCloseable { + override def close(): Unit = os.remove.all(this) +} + sealed trait PathConvertible[T] { def apply(t: T): java.nio.file.Path } diff --git a/os/src/TempOps.scala b/os/src/TempOps.scala index 900fe6b7..fc83cbc5 100644 --- a/os/src/TempOps.scala +++ b/os/src/TempOps.scala @@ -1,11 +1,23 @@ package os -import java.nio.file.attribute.{FileAttribute, PosixFilePermission, PosixFilePermissions} +import java.nio.file.attribute.{FileAttribute, PosixFilePermissions} +import scala.util.{Using, Try} /** - * Alias for `java.nio.file.Files.createTempFile` and - * `java.io.File.deleteOnExit`. Pass in `deleteOnExit = false` if you want - * the temp file to stick around. + * Create temporary files and directories. [[withTempFile]] and [[withTempDir]] + * are convenience methods that handle the most common case. They delete the temp + * file/dir immediately after the given function completed - even if the given + * function threw an exception. + * + * {{{ + * withTempFile { file => + * os.write(file, "some content") + * } + * }}} + * + * [[os.temp()]] and [[os.temp.dir()]] are aliases for + * `java.nio.file.Files.createTemp[File|Directory]` and `java.io.File.deleteOnExit`. + * Pass in `deleteOnExit = false` if you want the temp file to stick around. */ object temp { @@ -27,7 +39,7 @@ object temp { suffix: String = null, deleteOnExit: Boolean = true, perms: PermSet = null - ): Path = { + ): TempPath = { import collection.JavaConverters._ val permArray: Array[FileAttribute[_]] = if (perms == null) Array.empty @@ -40,7 +52,7 @@ object temp { if (contents != null) write.over(Path(nioPath), contents) if (deleteOnExit) nioPath.toFile.deleteOnExit() - Path(nioPath) + TempPath(nioPath) } /** @@ -56,7 +68,7 @@ object temp { prefix: String = null, deleteOnExit: Boolean = true, perms: PermSet = null - ): Path = { + ): TempPath = { val permArray: Array[FileAttribute[_]] = if (perms == null) Array.empty else Array(PosixFilePermissions.asFileAttribute(perms.toSet())) @@ -67,7 +79,38 @@ object temp { } if (deleteOnExit) nioPath.toFile.deleteOnExit() - Path(nioPath) + TempPath(nioPath) } + /** + * Convenience method that creates a temporary file and automatically deletes it + * after the given function completed - even if the function throws an exception. + * + * {{{ + * withTempFile { file => + * os.write(file, "some content") + * } + * }}} + */ + def withTempFile[A](fun: Path => A): Try[A] = + Using(os.temp( + deleteOnExit = false // TempFile.close() deletes it, no need to register with JVM + ))(fun) + + /** + * Convenience method that creates a temporary directory and automatically deletes it + * after the given function completed - even if the function throws an exception. + * + * {{{ + * withTempDir { file => + * val file = dir / "somefile" + * os.write(file, "some content") + * } + * }}} + */ + def withTempDir[A](fun: Path => A): Try[A] = + Using(os.temp.dir( + deleteOnExit = false // TempFile.close() deletes it, no need to register with JVM + ))(fun) + } diff --git a/os/test/src/TempPathTests.scala b/os/test/src/TempPathTests.scala new file mode 100644 index 00000000..93004049 --- /dev/null +++ b/os/test/src/TempPathTests.scala @@ -0,0 +1,73 @@ +package test.os + +import os._ +import os.temp.{withTempFile, withTempDir} +import scala.util.Using +import utest.{assert => _, _} + +object TempPathTests extends TestSuite{ + val tests = Tests { + + test("convenience methods") { + test("withTempFile") { + var tempFilePath: String = null + withTempFile { file => + tempFilePath = file.toString + assert(os.exists(file)) + } + assert(!os.exists(os.Path(tempFilePath)), s"temp file did not get auto-deleted after `Using` block: $tempFilePath") + } + test("withTempDir") { + var tempDirPath: String = null + var tempFilePath: String = null + withTempDir { dir => + val file = dir / "somefile" + tempDirPath = dir.toString + tempFilePath = file.toString + os.write(file, "some content") + assert(os.exists(dir)) + assert(os.exists(file)) + } + assert(!os.exists(os.Path(tempDirPath)), s"temp dir did not get auto-deleted after `Using` block: $tempDirPath") + } + } + + test("delete after `Using` block") { + test("single file") { + var tempFilePath: String = null + Using(os.temp()) { file => + tempFilePath = file.toString + assert(os.exists(file)) + } + assert(!os.exists(os.Path(tempFilePath)), s"temp file did not get auto-deleted after `Using` block: $tempFilePath") + } + test("directory") { + var tempDirPath: String = null + var tempFilePath: String = null + Using(os.temp.dir()) { dir => + val file = dir / "somefile" + tempDirPath = dir.toString + tempFilePath = file.toString + os.write(file, "some content") + assert(os.exists(dir)) + assert(os.exists(file)) + } + assert(!os.exists(os.Path(tempDirPath)), s"temp dir did not get auto-deleted after `Using` block: $tempDirPath") + } + + test("multiple files") { + var tempFilePaths: Seq[String] = Nil + Using.Manager { use => + val file1 = use(os.temp()) + val file2 = use(os.temp()) + val files = Seq(file1, file2) + tempFilePaths = files.map(_.toString) + files.foreach(file => assert(os.exists(file))) + } + tempFilePaths.foreach { file => + assert(!os.exists(os.Path(file)), s"temp file did not get auto-deleted after `Using` block: $file") + } + } + } + } +} From 6bfa0a17f87df428691387254009b6d8a0779433 Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Sat, 28 Jan 2023 18:01:27 +0100 Subject: [PATCH 02/14] return A directly, rather than Try[A] (PR comment) --- os/src/TempOps.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/os/src/TempOps.scala b/os/src/TempOps.scala index fc83cbc5..9868b917 100644 --- a/os/src/TempOps.scala +++ b/os/src/TempOps.scala @@ -1,7 +1,7 @@ package os import java.nio.file.attribute.{FileAttribute, PosixFilePermissions} -import scala.util.{Using, Try} +import scala.util.Using /** * Create temporary files and directories. [[withTempFile]] and [[withTempDir]] @@ -92,8 +92,8 @@ object temp { * } * }}} */ - def withTempFile[A](fun: Path => A): Try[A] = - Using(os.temp( + def withTempFile[A](fun: Path => A): A = + Using.resource(os.temp( deleteOnExit = false // TempFile.close() deletes it, no need to register with JVM ))(fun) @@ -108,8 +108,8 @@ object temp { * } * }}} */ - def withTempDir[A](fun: Path => A): Try[A] = - Using(os.temp.dir( + def withTempDir[A](fun: Path => A): A = + Using.resource(os.temp.dir( deleteOnExit = false // TempFile.close() deletes it, no need to register with JVM ))(fun) From 4ea3d1f81d111cf8ef461d17616b7e7c5aa19c55 Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Sat, 28 Jan 2023 18:14:49 +0100 Subject: [PATCH 03/14] rename to `os.temp.with[File|Dir]` --- Readme.adoc | 12 ++++++------ os/src/TempOps.scala | 12 ++++++------ os/test/src/TempPathTests.scala | 9 ++++----- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/Readme.adoc b/Readme.adoc index 7d2eca6f..869cac74 100644 --- a/Readme.adoc +++ b/Readme.adoc @@ -1157,11 +1157,11 @@ os.followLink(wd / "misc" / "folder-symlink") ==> Some(wd / "folder1") os.followLink(wd / "misc" / "broken-symlink") ==> None ---- -==== `os.temp.withTempFile` +==== `os.temp.withFile` [source,scala] ---- -os.temp.withTempFile[A](fun: Path => A): Try[A] +os.temp.withFile[A](fun: Path => A): A ---- Creates a temporary file, passes it to the given function and removes it immediately after the function completed, even if the function threw an exception. @@ -1170,16 +1170,16 @@ This doesn't rely on the JVM's `deleteOnExit` handler, therefor the temp file ge [source,scala] ---- -withTempFile { file => +os.temp.withFile { file => os.write(file, "some content") } ---- -==== `os.temp.withTempDir` +==== `os.temp.withDir` [source,scala] ---- -os.temp.withTempDir[A](fun: Path => A): Try[A] +os.temp.withDir[A](fun: Path => A): A ---- Creates a temporary directory, passes it to the given function and removes it immediately after the function completed, even if the function threw an exception. @@ -1188,7 +1188,7 @@ This doesn't rely on the JVM's `deleteOnExit` handler, therefor the temp dir get [source,scala] ---- -withTempDir { file => +os.temp.withDir { file => val file = dir / "somefile" os.write(file, "some content") } diff --git a/os/src/TempOps.scala b/os/src/TempOps.scala index 9868b917..12123a8e 100644 --- a/os/src/TempOps.scala +++ b/os/src/TempOps.scala @@ -4,13 +4,13 @@ import java.nio.file.attribute.{FileAttribute, PosixFilePermissions} import scala.util.Using /** - * Create temporary files and directories. [[withTempFile]] and [[withTempDir]] + * Create temporary files and directories. [[withFile]] and [[withDir]] * are convenience methods that handle the most common case. They delete the temp * file/dir immediately after the given function completed - even if the given * function threw an exception. * * {{{ - * withTempFile { file => + * os.temp.withFile { file => * os.write(file, "some content") * } * }}} @@ -87,12 +87,12 @@ object temp { * after the given function completed - even if the function throws an exception. * * {{{ - * withTempFile { file => + * os.temp.withFile { file => * os.write(file, "some content") * } * }}} */ - def withTempFile[A](fun: Path => A): A = + def withFile[A](fun: Path => A): A = Using.resource(os.temp( deleteOnExit = false // TempFile.close() deletes it, no need to register with JVM ))(fun) @@ -102,13 +102,13 @@ object temp { * after the given function completed - even if the function throws an exception. * * {{{ - * withTempDir { file => + * os.temp.withDir { file => * val file = dir / "somefile" * os.write(file, "some content") * } * }}} */ - def withTempDir[A](fun: Path => A): A = + def withDir[A](fun: Path => A): A = Using.resource(os.temp.dir( deleteOnExit = false // TempFile.close() deletes it, no need to register with JVM ))(fun) diff --git a/os/test/src/TempPathTests.scala b/os/test/src/TempPathTests.scala index 93004049..79ac819f 100644 --- a/os/test/src/TempPathTests.scala +++ b/os/test/src/TempPathTests.scala @@ -1,7 +1,6 @@ package test.os import os._ -import os.temp.{withTempFile, withTempDir} import scala.util.Using import utest.{assert => _, _} @@ -9,18 +8,18 @@ object TempPathTests extends TestSuite{ val tests = Tests { test("convenience methods") { - test("withTempFile") { + test("temp.withFile") { var tempFilePath: String = null - withTempFile { file => + os.temp.withFile { file => tempFilePath = file.toString assert(os.exists(file)) } assert(!os.exists(os.Path(tempFilePath)), s"temp file did not get auto-deleted after `Using` block: $tempFilePath") } - test("withTempDir") { + test("temp.withDir") { var tempDirPath: String = null var tempFilePath: String = null - withTempDir { dir => + os.temp.withDir { dir => val file = dir / "somefile" tempDirPath = dir.toString tempFilePath = file.toString From 4b164824ce6fdeddcb434422e730876bcec9e6dd Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Sat, 28 Jan 2023 18:20:48 +0100 Subject: [PATCH 04/14] provide the same set of options as we do in the apply methods --- os/src/TempOps.scala | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/os/src/TempOps.scala b/os/src/TempOps.scala index 12123a8e..5a5b8605 100644 --- a/os/src/TempOps.scala +++ b/os/src/TempOps.scala @@ -92,10 +92,23 @@ object temp { * } * }}} */ - def withFile[A](fun: Path => A): A = + def withFile[A]( + fun: Path => A, + contents: Source = null, + dir: Path = null, + prefix: String = null, + suffix: String = null, + perms: PermSet = null + ): A = { Using.resource(os.temp( - deleteOnExit = false // TempFile.close() deletes it, no need to register with JVM + contents = contents, + dir = dir, + prefix = prefix, + suffix = suffix, + deleteOnExit = false, // TempFile.close() deletes it, no need to register with JVM + perms = perms ))(fun) + } /** * Convenience method that creates a temporary directory and automatically deletes it @@ -108,9 +121,17 @@ object temp { * } * }}} */ - def withDir[A](fun: Path => A): A = + def withDir[A]( + fun: Path => A, + dir: Path = null, + prefix: String = null, + perms: PermSet = null + ): A = Using.resource(os.temp.dir( - deleteOnExit = false // TempFile.close() deletes it, no need to register with JVM + dir = dir, + prefix = prefix, + deleteOnExit = false, // TempFile.close() deletes it, no need to register with JVM + perms = perms ))(fun) } From 9a35dbac2fd525c0ff66748783eddd67f7a49c4e Mon Sep 17 00:00:00 2001 From: Tobias Roeser Date: Wed, 8 Feb 2023 22:55:04 +0100 Subject: [PATCH 05/14] Added Scala version specific source paths based on ranges A version with a `-` suffix targets code for all Scala versions smaller or equal. A version with a `+` suffix targets code for all Scala versions greater or equal. You can run `mill showNamed __.sources` to see all source dirs. --- build.sc | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/build.sc b/build.sc index bb723fa6..a5eb6a70 100644 --- a/build.sc +++ b/build.sc @@ -143,10 +143,15 @@ trait OsLibModule extends CrossScalaModule with PublishModule with AcyclicModule ) def platformSegment: String override def millSourcePath = super.millSourcePath / oslib.up - override def sources = T.sources( - millSourcePath / "src", - millSourcePath / s"src-$platformSegment" - ) + override def sources = T.sources { + Seq( + PathRef(millSourcePath / "src"), + PathRef(millSourcePath / s"src-$platformSegment") + ) ++ + ZincWorkerUtil.versionRanges(crossScalaVersion, scalaVersions).map(vr => + PathRef(millSourcePath / s"src-${vr}")) + + } } trait OsLibTestModule extends ScalaModule with TestModule.Utest with SafeDeps { From 40c2f1d1c6adc883f7a4ae2e46f9a8707c0b0be3 Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Sat, 11 Feb 2023 16:48:43 +0100 Subject: [PATCH 06/14] add `new` modifier for scala 2 --- os/src/TempOps.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/os/src/TempOps.scala b/os/src/TempOps.scala index 5a5b8605..ded05d1c 100644 --- a/os/src/TempOps.scala +++ b/os/src/TempOps.scala @@ -52,7 +52,7 @@ object temp { if (contents != null) write.over(Path(nioPath), contents) if (deleteOnExit) nioPath.toFile.deleteOnExit() - TempPath(nioPath) + new TempPath(nioPath) } /** @@ -79,7 +79,7 @@ object temp { } if (deleteOnExit) nioPath.toFile.deleteOnExit() - TempPath(nioPath) + new TempPath(nioPath) } /** From 76be191c4cee23b447948e9e717bea7ad0bc2b72 Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Sat, 11 Feb 2023 16:50:01 +0100 Subject: [PATCH 07/14] add scala.util.Using from stdlib for 2.12- builds --- os/src-2.12-/Using.scala | 395 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 os/src-2.12-/Using.scala diff --git a/os/src-2.12-/Using.scala b/os/src-2.12-/Using.scala new file mode 100644 index 00000000..bf0ed0cf --- /dev/null +++ b/os/src-2.12-/Using.scala @@ -0,0 +1,395 @@ +/* + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package scala.util + +import scala.util.control.{ControlThrowable, NonFatal} + +/** A utility for performing automatic resource management. It can be used to perform an + * operation using resources, after which it releases the resources in reverse order + * of their creation. + * + * ==Usage== + * + * There are multiple ways to automatically manage resources with `Using`. If you only need + * to manage a single resource, the [[Using.apply `apply`]] method is easiest; it wraps the + * resource opening, operation, and resource releasing in a `Try`. + * + * Example: + * {{{ + * import java.io.{BufferedReader, FileReader} + * import scala.util.{Try, Using} + * + * val lines: Try[Seq[String]] = + * Using(new BufferedReader(new FileReader("file.txt"))) { reader => + * Iterator.continually(reader.readLine()).takeWhile(_ != null).toSeq + * } + * }}} + * + * If you need to manage multiple resources, [[Using.Manager$.apply `Using.Manager`]] should + * be used. It allows the managing of arbitrarily many resources, whose creation, use, and + * release are all wrapped in a `Try`. + * + * Example: + * {{{ + * import java.io.{BufferedReader, FileReader} + * import scala.util.{Try, Using} + * + * val lines: Try[Seq[String]] = Using.Manager { use => + * val r1 = use(new BufferedReader(new FileReader("file1.txt"))) + * val r2 = use(new BufferedReader(new FileReader("file2.txt"))) + * val r3 = use(new BufferedReader(new FileReader("file3.txt"))) + * val r4 = use(new BufferedReader(new FileReader("file4.txt"))) + * + * // use your resources here + * def lines(reader: BufferedReader): Iterator[String] = + * Iterator.continually(reader.readLine()).takeWhile(_ != null) + * + * (lines(r1) ++ lines(r2) ++ lines(r3) ++ lines(r4)).toList + * } + * }}} + * + * If you wish to avoid wrapping management and operations in a `Try`, you can use + * [[Using.resource `Using.resource`]], which throws any exceptions that occur. + * + * Example: + * {{{ + * import java.io.{BufferedReader, FileReader} + * import scala.util.Using + * + * val lines: Seq[String] = + * Using.resource(new BufferedReader(new FileReader("file.txt"))) { reader => + * Iterator.continually(reader.readLine()).takeWhile(_ != null).toSeq + * } + * }}} + * + * ==Suppression Behavior== + * + * If two exceptions are thrown (e.g., by an operation and closing a resource), + * one of them is re-thrown, and the other is + * [[java.lang.Throwable#addSuppressed added to it as a suppressed exception]]. + * If the two exceptions are of different 'severities' (see below), the one of a higher + * severity is re-thrown, and the one of a lower severity is added to it as a suppressed + * exception. If the two exceptions are of the same severity, the one thrown first is + * re-thrown, and the one thrown second is added to it as a suppressed exception. + * If an exception is a [[scala.util.control.ControlThrowable `ControlThrowable`]], or + * if it does not support suppression (see + * [[java.lang.Throwable `Throwable`'s constructor with an `enableSuppression` parameter]]), + * an exception that would have been suppressed is instead discarded. + * + * Exceptions are ranked from highest to lowest severity as follows: + * - `java.lang.VirtualMachineError` + * - `java.lang.LinkageError` + * - `java.lang.InterruptedException` and `java.lang.ThreadDeath` + * - [[scala.util.control.NonFatal fatal exceptions]], excluding `scala.util.control.ControlThrowable` + * - `scala.util.control.ControlThrowable` + * - all other exceptions + * + * When more than two exceptions are thrown, the first two are combined and + * re-thrown as described above, and each successive exception thrown is combined + * as it is thrown. + * + * @define suppressionBehavior See the main doc for [[Using `Using`]] for full details of + * suppression behavior. + */ +object Using { + /** Performs an operation using a resource, and then releases the resource, + * even if the operation throws an exception. + * + * $suppressionBehavior + * + * @return a [[Try]] containing an exception if one or more were thrown, + * or the result of the operation if no exceptions were thrown + */ + def apply[R: Releasable, A](resource: => R)(f: R => A): Try[A] = Try { Using.resource(resource)(f) } + + /** A resource manager. + * + * Resources can be registered with the manager by calling [[acquire `acquire`]]; + * such resources will be released in reverse order of their acquisition + * when the manager is closed, regardless of any exceptions thrown + * during use. + * + * $suppressionBehavior + * + * @note It is recommended for API designers to require an implicit `Manager` + * for the creation of custom resources, and to call `acquire` during those + * resources' construction. Doing so guarantees that the resource ''must'' be + * automatically managed, and makes it impossible to forget to do so. + * + * + * Example: + * {{{ + * class SafeFileReader(file: File)(implicit manager: Using.Manager) + * extends BufferedReader(new FileReader(file)) { + * + * def this(fileName: String)(implicit manager: Using.Manager) = this(new File(fileName)) + * + * manager.acquire(this) + * } + * }}} + */ + final class Manager private { + import Manager._ + + private var closed = false + private[this] var resources: List[Resource[_]] = Nil + + /** Registers the specified resource with this manager, so that + * the resource is released when the manager is closed, and then + * returns the (unmodified) resource. + */ + def apply[R: Releasable](resource: R): R = { + acquire(resource) + resource + } + + /** Registers the specified resource with this manager, so that + * the resource is released when the manager is closed. + */ + def acquire[R: Releasable](resource: R): Unit = { + if (resource == null) throw new NullPointerException("null resource") + if (closed) throw new IllegalStateException("Manager has already been closed") + resources = new Resource(resource) :: resources + } + + private def manage[A](op: Manager => A): A = { + var toThrow: Throwable = null + try { + op(this) + } catch { + case t: Throwable => + toThrow = t + null.asInstanceOf[A] // compiler doesn't know `finally` will throw + } finally { + closed = true + var rs = resources + resources = null // allow GC, in case something is holding a reference to `this` + while (rs.nonEmpty) { + val resource = rs.head + rs = rs.tail + try resource.release() + catch { + case t: Throwable => + if (toThrow == null) toThrow = t + else toThrow = preferentiallySuppress(toThrow, t) + } + } + if (toThrow != null) throw toThrow + } + } + } + + object Manager { + /** Performs an operation using a `Manager`, then closes the `Manager`, + * releasing its resources (in reverse order of acquisition). + * + * Example: + * {{{ + * val lines = Using.Manager { use => + * use(new BufferedReader(new FileReader("file.txt"))).lines() + * } + * }}} + * + * If using resources which require an implicit `Manager` as a parameter, + * this method should be invoked with an `implicit` modifier before the function + * parameter: + * + * Example: + * {{{ + * val lines = Using.Manager { implicit use => + * new SafeFileReader("file.txt").lines() + * } + * }}} + * + * See the main doc for [[Using `Using`]] for full details of suppression behavior. + * + * @param op the operation to perform using the manager + * @tparam A the return type of the operation + * @return a [[Try]] containing an exception if one or more were thrown, + * or the result of the operation if no exceptions were thrown + */ + def apply[A](op: Manager => A): Try[A] = Try { (new Manager).manage(op) } + + private final class Resource[R](resource: R)(implicit releasable: Releasable[R]) { + def release(): Unit = releasable.release(resource) + } + } + + private def preferentiallySuppress(primary: Throwable, secondary: Throwable): Throwable = { + def score(t: Throwable): Int = t match { + case _: VirtualMachineError => 4 + case _: LinkageError => 3 + case _: InterruptedException | _: ThreadDeath => 2 + case _: ControlThrowable => 0 + case e if !NonFatal(e) => 1 // in case this method gets out of sync with NonFatal + case _ => -1 + } + @inline def suppress(t: Throwable, suppressed: Throwable): Throwable = { t.addSuppressed(suppressed); t } + + if (score(secondary) > score(primary)) suppress(secondary, primary) + else suppress(primary, secondary) + } + + /** Performs an operation using a resource, and then releases the resource, + * even if the operation throws an exception. This method behaves similarly + * to Java's try-with-resources. + * + * $suppressionBehavior + * + * @param resource the resource + * @param body the operation to perform with the resource + * @tparam R the type of the resource + * @tparam A the return type of the operation + * @return the result of the operation, if neither the operation nor + * releasing the resource throws + */ + def resource[R, A](resource: R)(body: R => A)(implicit releasable: Releasable[R]): A = { + if (resource == null) throw new NullPointerException("null resource") + + var toThrow: Throwable = null + try { + body(resource) + } catch { + case t: Throwable => + toThrow = t + null.asInstanceOf[A] // compiler doesn't know `finally` will throw + } finally { + if (toThrow eq null) releasable.release(resource) + else { + try releasable.release(resource) + catch { case other: Throwable => toThrow = preferentiallySuppress(toThrow, other) } + finally throw toThrow + } + } + } + + /** Performs an operation using two resources, and then releases the resources + * in reverse order, even if the operation throws an exception. This method + * behaves similarly to Java's try-with-resources. + * + * $suppressionBehavior + * + * @param resource1 the first resource + * @param resource2 the second resource + * @param body the operation to perform using the resources + * @tparam R1 the type of the first resource + * @tparam R2 the type of the second resource + * @tparam A the return type of the operation + * @return the result of the operation, if neither the operation nor + * releasing the resources throws + */ + def resources[R1: Releasable, R2: Releasable, A]( + resource1: R1, + resource2: => R2 + )(body: (R1, R2) => A + ): A = + resource(resource1) { r1 => + resource(resource2) { r2 => + body(r1, r2) + } + } + + /** Performs an operation using three resources, and then releases the resources + * in reverse order, even if the operation throws an exception. This method + * behaves similarly to Java's try-with-resources. + * + * $suppressionBehavior + * + * @param resource1 the first resource + * @param resource2 the second resource + * @param resource3 the third resource + * @param body the operation to perform using the resources + * @tparam R1 the type of the first resource + * @tparam R2 the type of the second resource + * @tparam R3 the type of the third resource + * @tparam A the return type of the operation + * @return the result of the operation, if neither the operation nor + * releasing the resources throws + */ + def resources[R1: Releasable, R2: Releasable, R3: Releasable, A]( + resource1: R1, + resource2: => R2, + resource3: => R3 + )(body: (R1, R2, R3) => A + ): A = + resource(resource1) { r1 => + resource(resource2) { r2 => + resource(resource3) { r3 => + body(r1, r2, r3) + } + } + } + + /** Performs an operation using four resources, and then releases the resources + * in reverse order, even if the operation throws an exception. This method + * behaves similarly to Java's try-with-resources. + * + * $suppressionBehavior + * + * @param resource1 the first resource + * @param resource2 the second resource + * @param resource3 the third resource + * @param resource4 the fourth resource + * @param body the operation to perform using the resources + * @tparam R1 the type of the first resource + * @tparam R2 the type of the second resource + * @tparam R3 the type of the third resource + * @tparam R4 the type of the fourth resource + * @tparam A the return type of the operation + * @return the result of the operation, if neither the operation nor + * releasing the resources throws + */ + def resources[R1: Releasable, R2: Releasable, R3: Releasable, R4: Releasable, A]( + resource1: R1, + resource2: => R2, + resource3: => R3, + resource4: => R4 + )(body: (R1, R2, R3, R4) => A + ): A = + resource(resource1) { r1 => + resource(resource2) { r2 => + resource(resource3) { r3 => + resource(resource4) { r4 => + body(r1, r2, r3, r4) + } + } + } + } + + /** A type class describing how to release a particular type of resource. + * + * A resource is anything which needs to be released, closed, or otherwise cleaned up + * in some way after it is finished being used, and for which waiting for the object's + * garbage collection to be cleaned up would be unacceptable. For example, an instance of + * [[java.io.OutputStream]] would be considered a resource, because it is important to close + * the stream after it is finished being used. + * + * An instance of `Releasable` is needed in order to automatically manage a resource + * with [[Using `Using`]]. An implicit instance is provided for all types extending + * [[java.lang.AutoCloseable]]. + * + * @tparam R the type of the resource + */ + trait Releasable[-R] { + /** Releases the specified resource. */ + def release(resource: R): Unit + } + + object Releasable { + /** An implicit `Releasable` for [[java.lang.AutoCloseable `AutoCloseable`s]]. */ + implicit object AutoCloseableIsReleasable extends Releasable[AutoCloseable] { + def release(resource: AutoCloseable): Unit = resource.close() + } + } + +} From aea113bf9cbf385623c342720625a22516bfd1be Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Sat, 11 Feb 2023 17:11:20 +0100 Subject: [PATCH 08/14] change namespace of scala.util.Using to avoid any clashes in dowstream projects. --- os/src/TempOps.scala | 2 +- os/{src-2.12- => src}/Using.scala | 7 ++++++- os/test/src/TempPathTests.scala | 3 ++- 3 files changed, 9 insertions(+), 3 deletions(-) rename os/{src-2.12- => src}/Using.scala (98%) diff --git a/os/src/TempOps.scala b/os/src/TempOps.scala index ded05d1c..9c7836fd 100644 --- a/os/src/TempOps.scala +++ b/os/src/TempOps.scala @@ -1,7 +1,7 @@ package os import java.nio.file.attribute.{FileAttribute, PosixFilePermissions} -import scala.util.Using +import os.util.Using /** * Create temporary files and directories. [[withFile]] and [[withDir]] diff --git a/os/src-2.12-/Using.scala b/os/src/Using.scala similarity index 98% rename from os/src-2.12-/Using.scala rename to os/src/Using.scala index bf0ed0cf..eeafb868 100644 --- a/os/src-2.12-/Using.scala +++ b/os/src/Using.scala @@ -1,4 +1,8 @@ /* + * Verbatim copy of scala.util.Using - only copied here because we're also targeting + * scala 2.12 and 2.11, which don't ship that by default. Changed the package to + * os.util to avoid any clashes in dowstream projects. + * * Scala (https://www.scala-lang.org) * * Copyright EPFL and Lightbend, Inc. @@ -10,8 +14,9 @@ * additional information regarding copyright ownership. */ -package scala.util +package os.util +import scala.util.Try import scala.util.control.{ControlThrowable, NonFatal} /** A utility for performing automatic resource management. It can be used to perform an diff --git a/os/test/src/TempPathTests.scala b/os/test/src/TempPathTests.scala index 79ac819f..cbcad556 100644 --- a/os/test/src/TempPathTests.scala +++ b/os/test/src/TempPathTests.scala @@ -1,7 +1,8 @@ package test.os import os._ -import scala.util.Using +// if running with scala 2.13+, this can be the regular `scala.util.Using` +import os.util.Using import utest.{assert => _, _} object TempPathTests extends TestSuite{ From 304a9b869857aa87b96bd4098fa61fd49dc4ef62 Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Fri, 17 Feb 2023 14:31:51 +0100 Subject: [PATCH 09/14] use custom `removeRecursively` function, to please scala2 compiler... For some reason, calling `os.remove.all(this)`: ```scala class TempPath private[os] (wrapped: java.nio.file.Path) extends Path(wrapped) with AutoCloseable { override def close(): Unit = os.remove.all(this) } ``` fails in Scala 2, with rather obscure compiler errors: ``` [error] Unwanted cyclic dependency [info] symbol: object all [info] symbol: value up [info] More dependencies at lines 394 394 71 87 71 152 152 87 [info] /home/mp/Projects/os-lib/os/src-jvm/package.scala:13:34: [info] def resource(implicit resRoot: ResourceRoot = Thread.currentThread().getContextClassLoader) = { [info] ^ [info] symbol: trait ResourceRoot [info] More dependencies at lines 14 14 [info] /home/mp/Projects/os-lib/os/src-jvm/ResourcePath.scala:19:49: [info] extends BasePathImpl with ReadablePath with SegmentedPath { [info] ^ [info] symbol: trait SegmentedPath [info] More dependencies at lines 44 36 18 31 44 18 18 31 [error] 6 errors found ``` --- os/src/Path.scala | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/os/src/Path.scala b/os/src/Path.scala index 898f8080..de1ecdcf 100644 --- a/os/src/Path.scala +++ b/os/src/Path.scala @@ -2,6 +2,7 @@ package os import java.net.URI import java.nio.file.Paths +import java.nio.file.Files import collection.JavaConverters._ import scala.language.implicitConversions @@ -281,7 +282,7 @@ class RelPath private[os] (segments0: Array[String], val ups: Int) case _ => false } - def toNIO = java.nio.file.Paths.get(toString) + def toNIO = Paths.get(toString) def asSubPath = { require(ups == 0) @@ -342,7 +343,7 @@ class SubPath private[os] (val segments0: Array[String]) case _ => false } - def toNIO = java.nio.file.Paths.get(toString) + def toNIO = Paths.get(toString) def resolveFrom(base: os.Path) = base / this } @@ -448,7 +449,7 @@ trait ReadablePath { class Path private[os] (val wrapped: java.nio.file.Path) extends FilePath with ReadablePath with BasePathImpl { def toSource: SeekableSource = - new SeekableSource.ChannelSource(java.nio.file.Files.newByteChannel(wrapped)) + new SeekableSource.ChannelSource(Files.newByteChannel(wrapped)) require(wrapped.isAbsolute, s"$wrapped is not an absolute path") def segments: Iterator[String] = wrapped.iterator().asScala.map(_.toString) @@ -495,12 +496,23 @@ class Path private[os] (val wrapped: java.nio.file.Path) def resolveFrom(base: os.Path) = this - def getInputStream = java.nio.file.Files.newInputStream(wrapped) + def getInputStream = Files.newInputStream(wrapped) } class TempPath private[os] (wrapped: java.nio.file.Path) extends Path(wrapped) with AutoCloseable { - override def close(): Unit = os.remove.all(this) + + override def close(): Unit = deleteRecursively(wrapped) + + /** Wouldn't it be nice if we could just call `os.remove.all(this)`? + * For some reason, Scala 2 throws a rather obscure `[error] Unwanted cyclic dependency` + */ + private def deleteRecursively(ioPath: java.nio.file.Path): Unit = { + if (Files.isDirectory(ioPath)) { + Files.list(ioPath).forEach(deleteRecursively) + } + Files.deleteIfExists(ioPath) + } } sealed trait PathConvertible[T] { From e2da3fd05cf0fc9084bc0e19ee4b482a69d74284 Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Fri, 17 Feb 2023 16:01:18 +0100 Subject: [PATCH 10/14] ugliness for Scala 2.11 support --- os/src/Path.scala | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/os/src/Path.scala b/os/src/Path.scala index de1ecdcf..7fd4ddc2 100644 --- a/os/src/Path.scala +++ b/os/src/Path.scala @@ -1,7 +1,7 @@ package os import java.net.URI -import java.nio.file.Paths +import java.nio.file.{LinkOption, Paths} import java.nio.file.Files import collection.JavaConverters._ @@ -282,7 +282,7 @@ class RelPath private[os] (segments0: Array[String], val ups: Int) case _ => false } - def toNIO = Paths.get(toString) + def toNIO = java.nio.file.Paths.get(toString) def asSubPath = { require(ups == 0) @@ -343,7 +343,7 @@ class SubPath private[os] (val segments0: Array[String]) case _ => false } - def toNIO = Paths.get(toString) + def toNIO = java.nio.file.Paths.get(toString) def resolveFrom(base: os.Path) = base / this } @@ -509,7 +509,16 @@ class TempPath private[os] (wrapped: java.nio.file.Path) */ private def deleteRecursively(ioPath: java.nio.file.Path): Unit = { if (Files.isDirectory(ioPath)) { - Files.list(ioPath).forEach(deleteRecursively) + // while we support Scala 2.11 we need (something like) this: + Files.list(ioPath).forEach( + new java.util.function.Consumer[java.nio.file.Path] { + override def accept(path: java.nio.file.Path): Unit = + deleteRecursively(path) + } + ) + + // this works for Scala 2.12+ + // Files.list(ioPath).forEach(deleteRecursively) } Files.deleteIfExists(ioPath) } From e93cbdc2b60ffbdf1ee1d0e9f24ccae4556d4410 Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Fri, 17 Feb 2023 16:16:45 +0100 Subject: [PATCH 11/14] reducing line length to <= 100 chars - I miss scalafmt --- os/src/Using.scala | 13 +++++++++---- os/test/src/TempPathTests.scala | 15 ++++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/os/src/Using.scala b/os/src/Using.scala index eeafb868..4927a9ad 100644 --- a/os/src/Using.scala +++ b/os/src/Using.scala @@ -95,7 +95,8 @@ import scala.util.control.{ControlThrowable, NonFatal} * - `java.lang.VirtualMachineError` * - `java.lang.LinkageError` * - `java.lang.InterruptedException` and `java.lang.ThreadDeath` - * - [[scala.util.control.NonFatal fatal exceptions]], excluding `scala.util.control.ControlThrowable` + * - [[scala.util.control.NonFatal fatal exceptions]], excluding + * `scala.util.control.ControlThrowable` * - `scala.util.control.ControlThrowable` * - all other exceptions * @@ -115,7 +116,8 @@ object Using { * @return a [[Try]] containing an exception if one or more were thrown, * or the result of the operation if no exceptions were thrown */ - def apply[R: Releasable, A](resource: => R)(f: R => A): Try[A] = Try { Using.resource(resource)(f) } + def apply[R: Releasable, A](resource: => R)(f: R => A): Try[A] = + Try { Using.resource(resource)(f) } /** A resource manager. * @@ -236,10 +238,13 @@ object Using { case _: LinkageError => 3 case _: InterruptedException | _: ThreadDeath => 2 case _: ControlThrowable => 0 - case e if !NonFatal(e) => 1 // in case this method gets out of sync with NonFatal + case e if !NonFatal(e) => + 1 // in case this method gets out of sync with NonFatal case _ => -1 } - @inline def suppress(t: Throwable, suppressed: Throwable): Throwable = { t.addSuppressed(suppressed); t } + @inline def suppress(t: Throwable, suppressed: Throwable): Throwable = { + t.addSuppressed(suppressed); t + } if (score(secondary) > score(primary)) suppress(secondary, primary) else suppress(primary, secondary) diff --git a/os/test/src/TempPathTests.scala b/os/test/src/TempPathTests.scala index cbcad556..55729f01 100644 --- a/os/test/src/TempPathTests.scala +++ b/os/test/src/TempPathTests.scala @@ -15,7 +15,8 @@ object TempPathTests extends TestSuite{ tempFilePath = file.toString assert(os.exists(file)) } - assert(!os.exists(os.Path(tempFilePath)), s"temp file did not get auto-deleted after `Using` block: $tempFilePath") + assert(!os.exists(os.Path(tempFilePath)), + s"temp file did not get auto-deleted after `Using` block: $tempFilePath") } test("temp.withDir") { var tempDirPath: String = null @@ -28,7 +29,8 @@ object TempPathTests extends TestSuite{ assert(os.exists(dir)) assert(os.exists(file)) } - assert(!os.exists(os.Path(tempDirPath)), s"temp dir did not get auto-deleted after `Using` block: $tempDirPath") + assert(!os.exists(os.Path(tempDirPath)), + s"temp dir did not get auto-deleted after `Using` block: $tempDirPath") } } @@ -39,7 +41,8 @@ object TempPathTests extends TestSuite{ tempFilePath = file.toString assert(os.exists(file)) } - assert(!os.exists(os.Path(tempFilePath)), s"temp file did not get auto-deleted after `Using` block: $tempFilePath") + assert(!os.exists(os.Path(tempFilePath)), + s"temp file did not get auto-deleted after `Using` block: $tempFilePath") } test("directory") { var tempDirPath: String = null @@ -52,7 +55,8 @@ object TempPathTests extends TestSuite{ assert(os.exists(dir)) assert(os.exists(file)) } - assert(!os.exists(os.Path(tempDirPath)), s"temp dir did not get auto-deleted after `Using` block: $tempDirPath") + assert(!os.exists(os.Path(tempDirPath)), + s"temp dir did not get auto-deleted after `Using` block: $tempDirPath") } test("multiple files") { @@ -65,7 +69,8 @@ object TempPathTests extends TestSuite{ files.foreach(file => assert(os.exists(file))) } tempFilePaths.foreach { file => - assert(!os.exists(os.Path(file)), s"temp file did not get auto-deleted after `Using` block: $file") + assert(!os.exists(os.Path(file)), + s"temp file did not get auto-deleted after `Using` block: $file") } } } From 0b8bfef0007a0164b91ac4b188779fb2ce214c62 Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Tue, 14 Mar 2023 09:04:43 +0100 Subject: [PATCH 12/14] use scala-collection-compat for scala 2.12- --- build.sc | 11 +- os/src/TempOps.scala | 2 +- os/src/Using.scala | 405 -------------------------------- os/test/src/TempPathTests.scala | 3 +- 4 files changed, 10 insertions(+), 411 deletions(-) delete mode 100644 os/src/Using.scala diff --git a/build.sc b/build.sc index a5eb6a70..7d578847 100644 --- a/build.sc +++ b/build.sc @@ -35,6 +35,7 @@ object Deps { val acyclic = ivy"com.lihaoyi:::acyclic:0.3.6" val jna = ivy"net.java.dev.jna:jna:5.13.0" val geny = ivy"com.lihaoyi::geny::1.0.0" + val scalaCollectionCompat = ivy"org.scala-lang.modules::scala-collection-compat::2.9.0" val sourcecode = ivy"com.lihaoyi::sourcecode::0.3.0" val utest = ivy"com.lihaoyi::utest::0.8.1" def scalaLibrary(version: String) = ivy"org.scala-lang:scala-library:${version}" @@ -173,9 +174,13 @@ trait OsLibTestModule extends ScalaModule with TestModule.Utest with SafeDeps { trait OsModule extends OsLibModule { override def artifactName = "os-lib" - override def ivyDeps = Agg( - Deps.geny - ) + override def ivyDeps = T { + val scalaV = scalaVersion() + if (scalaV.startsWith("2.11") || scalaV.startsWith("2.12")) { + // include collection compat, mostly for a backported scala.util.Using + Agg(Deps.geny, Deps.scalaCollectionCompat) + } else Agg(Deps.geny) + } } trait WatchModule extends OsLibModule { diff --git a/os/src/TempOps.scala b/os/src/TempOps.scala index 9c7836fd..ded05d1c 100644 --- a/os/src/TempOps.scala +++ b/os/src/TempOps.scala @@ -1,7 +1,7 @@ package os import java.nio.file.attribute.{FileAttribute, PosixFilePermissions} -import os.util.Using +import scala.util.Using /** * Create temporary files and directories. [[withFile]] and [[withDir]] diff --git a/os/src/Using.scala b/os/src/Using.scala deleted file mode 100644 index 4927a9ad..00000000 --- a/os/src/Using.scala +++ /dev/null @@ -1,405 +0,0 @@ -/* - * Verbatim copy of scala.util.Using - only copied here because we're also targeting - * scala 2.12 and 2.11, which don't ship that by default. Changed the package to - * os.util to avoid any clashes in dowstream projects. - * - * Scala (https://www.scala-lang.org) - * - * Copyright EPFL and Lightbend, Inc. - * - * Licensed under Apache License 2.0 - * (http://www.apache.org/licenses/LICENSE-2.0). - * - * See the NOTICE file distributed with this work for - * additional information regarding copyright ownership. - */ - -package os.util - -import scala.util.Try -import scala.util.control.{ControlThrowable, NonFatal} - -/** A utility for performing automatic resource management. It can be used to perform an - * operation using resources, after which it releases the resources in reverse order - * of their creation. - * - * ==Usage== - * - * There are multiple ways to automatically manage resources with `Using`. If you only need - * to manage a single resource, the [[Using.apply `apply`]] method is easiest; it wraps the - * resource opening, operation, and resource releasing in a `Try`. - * - * Example: - * {{{ - * import java.io.{BufferedReader, FileReader} - * import scala.util.{Try, Using} - * - * val lines: Try[Seq[String]] = - * Using(new BufferedReader(new FileReader("file.txt"))) { reader => - * Iterator.continually(reader.readLine()).takeWhile(_ != null).toSeq - * } - * }}} - * - * If you need to manage multiple resources, [[Using.Manager$.apply `Using.Manager`]] should - * be used. It allows the managing of arbitrarily many resources, whose creation, use, and - * release are all wrapped in a `Try`. - * - * Example: - * {{{ - * import java.io.{BufferedReader, FileReader} - * import scala.util.{Try, Using} - * - * val lines: Try[Seq[String]] = Using.Manager { use => - * val r1 = use(new BufferedReader(new FileReader("file1.txt"))) - * val r2 = use(new BufferedReader(new FileReader("file2.txt"))) - * val r3 = use(new BufferedReader(new FileReader("file3.txt"))) - * val r4 = use(new BufferedReader(new FileReader("file4.txt"))) - * - * // use your resources here - * def lines(reader: BufferedReader): Iterator[String] = - * Iterator.continually(reader.readLine()).takeWhile(_ != null) - * - * (lines(r1) ++ lines(r2) ++ lines(r3) ++ lines(r4)).toList - * } - * }}} - * - * If you wish to avoid wrapping management and operations in a `Try`, you can use - * [[Using.resource `Using.resource`]], which throws any exceptions that occur. - * - * Example: - * {{{ - * import java.io.{BufferedReader, FileReader} - * import scala.util.Using - * - * val lines: Seq[String] = - * Using.resource(new BufferedReader(new FileReader("file.txt"))) { reader => - * Iterator.continually(reader.readLine()).takeWhile(_ != null).toSeq - * } - * }}} - * - * ==Suppression Behavior== - * - * If two exceptions are thrown (e.g., by an operation and closing a resource), - * one of them is re-thrown, and the other is - * [[java.lang.Throwable#addSuppressed added to it as a suppressed exception]]. - * If the two exceptions are of different 'severities' (see below), the one of a higher - * severity is re-thrown, and the one of a lower severity is added to it as a suppressed - * exception. If the two exceptions are of the same severity, the one thrown first is - * re-thrown, and the one thrown second is added to it as a suppressed exception. - * If an exception is a [[scala.util.control.ControlThrowable `ControlThrowable`]], or - * if it does not support suppression (see - * [[java.lang.Throwable `Throwable`'s constructor with an `enableSuppression` parameter]]), - * an exception that would have been suppressed is instead discarded. - * - * Exceptions are ranked from highest to lowest severity as follows: - * - `java.lang.VirtualMachineError` - * - `java.lang.LinkageError` - * - `java.lang.InterruptedException` and `java.lang.ThreadDeath` - * - [[scala.util.control.NonFatal fatal exceptions]], excluding - * `scala.util.control.ControlThrowable` - * - `scala.util.control.ControlThrowable` - * - all other exceptions - * - * When more than two exceptions are thrown, the first two are combined and - * re-thrown as described above, and each successive exception thrown is combined - * as it is thrown. - * - * @define suppressionBehavior See the main doc for [[Using `Using`]] for full details of - * suppression behavior. - */ -object Using { - /** Performs an operation using a resource, and then releases the resource, - * even if the operation throws an exception. - * - * $suppressionBehavior - * - * @return a [[Try]] containing an exception if one or more were thrown, - * or the result of the operation if no exceptions were thrown - */ - def apply[R: Releasable, A](resource: => R)(f: R => A): Try[A] = - Try { Using.resource(resource)(f) } - - /** A resource manager. - * - * Resources can be registered with the manager by calling [[acquire `acquire`]]; - * such resources will be released in reverse order of their acquisition - * when the manager is closed, regardless of any exceptions thrown - * during use. - * - * $suppressionBehavior - * - * @note It is recommended for API designers to require an implicit `Manager` - * for the creation of custom resources, and to call `acquire` during those - * resources' construction. Doing so guarantees that the resource ''must'' be - * automatically managed, and makes it impossible to forget to do so. - * - * - * Example: - * {{{ - * class SafeFileReader(file: File)(implicit manager: Using.Manager) - * extends BufferedReader(new FileReader(file)) { - * - * def this(fileName: String)(implicit manager: Using.Manager) = this(new File(fileName)) - * - * manager.acquire(this) - * } - * }}} - */ - final class Manager private { - import Manager._ - - private var closed = false - private[this] var resources: List[Resource[_]] = Nil - - /** Registers the specified resource with this manager, so that - * the resource is released when the manager is closed, and then - * returns the (unmodified) resource. - */ - def apply[R: Releasable](resource: R): R = { - acquire(resource) - resource - } - - /** Registers the specified resource with this manager, so that - * the resource is released when the manager is closed. - */ - def acquire[R: Releasable](resource: R): Unit = { - if (resource == null) throw new NullPointerException("null resource") - if (closed) throw new IllegalStateException("Manager has already been closed") - resources = new Resource(resource) :: resources - } - - private def manage[A](op: Manager => A): A = { - var toThrow: Throwable = null - try { - op(this) - } catch { - case t: Throwable => - toThrow = t - null.asInstanceOf[A] // compiler doesn't know `finally` will throw - } finally { - closed = true - var rs = resources - resources = null // allow GC, in case something is holding a reference to `this` - while (rs.nonEmpty) { - val resource = rs.head - rs = rs.tail - try resource.release() - catch { - case t: Throwable => - if (toThrow == null) toThrow = t - else toThrow = preferentiallySuppress(toThrow, t) - } - } - if (toThrow != null) throw toThrow - } - } - } - - object Manager { - /** Performs an operation using a `Manager`, then closes the `Manager`, - * releasing its resources (in reverse order of acquisition). - * - * Example: - * {{{ - * val lines = Using.Manager { use => - * use(new BufferedReader(new FileReader("file.txt"))).lines() - * } - * }}} - * - * If using resources which require an implicit `Manager` as a parameter, - * this method should be invoked with an `implicit` modifier before the function - * parameter: - * - * Example: - * {{{ - * val lines = Using.Manager { implicit use => - * new SafeFileReader("file.txt").lines() - * } - * }}} - * - * See the main doc for [[Using `Using`]] for full details of suppression behavior. - * - * @param op the operation to perform using the manager - * @tparam A the return type of the operation - * @return a [[Try]] containing an exception if one or more were thrown, - * or the result of the operation if no exceptions were thrown - */ - def apply[A](op: Manager => A): Try[A] = Try { (new Manager).manage(op) } - - private final class Resource[R](resource: R)(implicit releasable: Releasable[R]) { - def release(): Unit = releasable.release(resource) - } - } - - private def preferentiallySuppress(primary: Throwable, secondary: Throwable): Throwable = { - def score(t: Throwable): Int = t match { - case _: VirtualMachineError => 4 - case _: LinkageError => 3 - case _: InterruptedException | _: ThreadDeath => 2 - case _: ControlThrowable => 0 - case e if !NonFatal(e) => - 1 // in case this method gets out of sync with NonFatal - case _ => -1 - } - @inline def suppress(t: Throwable, suppressed: Throwable): Throwable = { - t.addSuppressed(suppressed); t - } - - if (score(secondary) > score(primary)) suppress(secondary, primary) - else suppress(primary, secondary) - } - - /** Performs an operation using a resource, and then releases the resource, - * even if the operation throws an exception. This method behaves similarly - * to Java's try-with-resources. - * - * $suppressionBehavior - * - * @param resource the resource - * @param body the operation to perform with the resource - * @tparam R the type of the resource - * @tparam A the return type of the operation - * @return the result of the operation, if neither the operation nor - * releasing the resource throws - */ - def resource[R, A](resource: R)(body: R => A)(implicit releasable: Releasable[R]): A = { - if (resource == null) throw new NullPointerException("null resource") - - var toThrow: Throwable = null - try { - body(resource) - } catch { - case t: Throwable => - toThrow = t - null.asInstanceOf[A] // compiler doesn't know `finally` will throw - } finally { - if (toThrow eq null) releasable.release(resource) - else { - try releasable.release(resource) - catch { case other: Throwable => toThrow = preferentiallySuppress(toThrow, other) } - finally throw toThrow - } - } - } - - /** Performs an operation using two resources, and then releases the resources - * in reverse order, even if the operation throws an exception. This method - * behaves similarly to Java's try-with-resources. - * - * $suppressionBehavior - * - * @param resource1 the first resource - * @param resource2 the second resource - * @param body the operation to perform using the resources - * @tparam R1 the type of the first resource - * @tparam R2 the type of the second resource - * @tparam A the return type of the operation - * @return the result of the operation, if neither the operation nor - * releasing the resources throws - */ - def resources[R1: Releasable, R2: Releasable, A]( - resource1: R1, - resource2: => R2 - )(body: (R1, R2) => A - ): A = - resource(resource1) { r1 => - resource(resource2) { r2 => - body(r1, r2) - } - } - - /** Performs an operation using three resources, and then releases the resources - * in reverse order, even if the operation throws an exception. This method - * behaves similarly to Java's try-with-resources. - * - * $suppressionBehavior - * - * @param resource1 the first resource - * @param resource2 the second resource - * @param resource3 the third resource - * @param body the operation to perform using the resources - * @tparam R1 the type of the first resource - * @tparam R2 the type of the second resource - * @tparam R3 the type of the third resource - * @tparam A the return type of the operation - * @return the result of the operation, if neither the operation nor - * releasing the resources throws - */ - def resources[R1: Releasable, R2: Releasable, R3: Releasable, A]( - resource1: R1, - resource2: => R2, - resource3: => R3 - )(body: (R1, R2, R3) => A - ): A = - resource(resource1) { r1 => - resource(resource2) { r2 => - resource(resource3) { r3 => - body(r1, r2, r3) - } - } - } - - /** Performs an operation using four resources, and then releases the resources - * in reverse order, even if the operation throws an exception. This method - * behaves similarly to Java's try-with-resources. - * - * $suppressionBehavior - * - * @param resource1 the first resource - * @param resource2 the second resource - * @param resource3 the third resource - * @param resource4 the fourth resource - * @param body the operation to perform using the resources - * @tparam R1 the type of the first resource - * @tparam R2 the type of the second resource - * @tparam R3 the type of the third resource - * @tparam R4 the type of the fourth resource - * @tparam A the return type of the operation - * @return the result of the operation, if neither the operation nor - * releasing the resources throws - */ - def resources[R1: Releasable, R2: Releasable, R3: Releasable, R4: Releasable, A]( - resource1: R1, - resource2: => R2, - resource3: => R3, - resource4: => R4 - )(body: (R1, R2, R3, R4) => A - ): A = - resource(resource1) { r1 => - resource(resource2) { r2 => - resource(resource3) { r3 => - resource(resource4) { r4 => - body(r1, r2, r3, r4) - } - } - } - } - - /** A type class describing how to release a particular type of resource. - * - * A resource is anything which needs to be released, closed, or otherwise cleaned up - * in some way after it is finished being used, and for which waiting for the object's - * garbage collection to be cleaned up would be unacceptable. For example, an instance of - * [[java.io.OutputStream]] would be considered a resource, because it is important to close - * the stream after it is finished being used. - * - * An instance of `Releasable` is needed in order to automatically manage a resource - * with [[Using `Using`]]. An implicit instance is provided for all types extending - * [[java.lang.AutoCloseable]]. - * - * @tparam R the type of the resource - */ - trait Releasable[-R] { - /** Releases the specified resource. */ - def release(resource: R): Unit - } - - object Releasable { - /** An implicit `Releasable` for [[java.lang.AutoCloseable `AutoCloseable`s]]. */ - implicit object AutoCloseableIsReleasable extends Releasable[AutoCloseable] { - def release(resource: AutoCloseable): Unit = resource.close() - } - } - -} diff --git a/os/test/src/TempPathTests.scala b/os/test/src/TempPathTests.scala index 55729f01..1424d7a2 100644 --- a/os/test/src/TempPathTests.scala +++ b/os/test/src/TempPathTests.scala @@ -1,8 +1,7 @@ package test.os import os._ -// if running with scala 2.13+, this can be the regular `scala.util.Using` -import os.util.Using +import scala.util.Using import utest.{assert => _, _} object TempPathTests extends TestSuite{ From 6edb4f24ccec834961af57d11ba32eccf9acf71b Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Wed, 22 Nov 2023 15:26:58 +0100 Subject: [PATCH 13/14] Update os/src/TempOps.scala Co-authored-by: Florian Schmaus --- os/src/TempOps.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os/src/TempOps.scala b/os/src/TempOps.scala index ded05d1c..83b5d705 100644 --- a/os/src/TempOps.scala +++ b/os/src/TempOps.scala @@ -105,7 +105,7 @@ object temp { dir = dir, prefix = prefix, suffix = suffix, - deleteOnExit = false, // TempFile.close() deletes it, no need to register with JVM + deleteOnExit = false, // TempPath.close() deletes it, no need to register with JVM perms = perms ))(fun) } From 7e6c994e244444dae99c969f58c715cde9337a91 Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Wed, 22 Nov 2023 15:27:05 +0100 Subject: [PATCH 14/14] Update os/src/TempOps.scala Co-authored-by: Florian Schmaus --- os/src/TempOps.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os/src/TempOps.scala b/os/src/TempOps.scala index 83b5d705..43284792 100644 --- a/os/src/TempOps.scala +++ b/os/src/TempOps.scala @@ -130,7 +130,7 @@ object temp { Using.resource(os.temp.dir( dir = dir, prefix = prefix, - deleteOnExit = false, // TempFile.close() deletes it, no need to register with JVM + deleteOnExit = false, // TempPath.close() deletes it, no need to register with JVM perms = perms ))(fun)