├── .editorconfig ├── .github ├── pull_request_template.md ├── renovate.json5 └── workflows │ ├── .java-version │ ├── build.yaml │ ├── gradle-wrapper.yaml │ └── release.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── RELEASING.md ├── api ├── finalization-hook.api └── finalization-hook.klib.api ├── build.gradle ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── src ├── commonMain │ └── kotlin │ │ └── com │ │ └── jakewharton │ │ └── finalization │ │ └── hook.kt ├── commonTest │ └── kotlin │ │ └── com │ │ └── jakewharton │ │ └── finalization │ │ └── FinalizationHookTest.kt ├── jvmMain │ └── kotlin │ │ └── com │ │ └── jakewharton │ │ └── finalization │ │ └── hook.kt ├── mingwMain │ └── kotlin │ │ └── com │ │ └── jakewharton │ │ └── finalization │ │ └── hook.kt ├── nativeMain │ └── kotlin │ │ └── com │ │ └── jakewharton │ │ └── finalization │ │ └── hook.kt └── posixMain │ └── kotlin │ └── com │ └── jakewharton │ └── finalization │ └── hook.kt └── test-app ├── build.gradle └── src └── commonMain └── kotlin └── example └── main.kt /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.yaml] 12 | indent_style = space 13 | 14 | [*.md] 15 | indent_style = space 16 | 17 | [*.{kt,kts}] 18 | ij_kotlin_imports_layout=* 19 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - [ ] `CHANGELOG.md`'s "Unreleased" section has been updated, if applicable. 4 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: 'https://docs.renovatebot.com/renovate-schema.json', 3 | extends: [ 4 | 'config:recommended', 5 | ], 6 | ignorePresets: [ 7 | // Ensure we get the latest version and are not pinned to old versions. 8 | 'workarounds:javaLTSVersions', 9 | ], 10 | customManagers: [ 11 | // Update .java-version file with the latest JDK version. 12 | { 13 | customType: 'regex', 14 | fileMatch: [ 15 | '\\.java-version$', 16 | ], 17 | matchStrings: [ 18 | '(?.*)\\n', 19 | ], 20 | datasourceTemplate: 'java-version', 21 | depNameTemplate: 'java', 22 | // Only write the major version. 23 | extractVersionTemplate: '^(?\\d+)', 24 | }, 25 | ], 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/.java-version: -------------------------------------------------------------------------------- 1 | 24 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: {} 5 | workflow_dispatch: {} 6 | push: 7 | branches: 8 | - 'trunk' 9 | tags-ignore: 10 | - '**' 11 | 12 | jobs: 13 | build: 14 | runs-on: macos-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-java@v4 19 | with: 20 | distribution: 'zulu' 21 | java-version-file: .github/workflows/.java-version 22 | 23 | - run: ./gradlew build 24 | 25 | - run: ./gradlew publish 26 | if: ${{ github.ref == 'refs/heads/trunk' && github.repository == 'JakeWharton/finalization-hook' }} 27 | env: 28 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_CENTRAL_USERNAME }} 29 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_CENTRAL_PASSWORD }} 30 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_PRIVATE_KEY }} 31 | 32 | - name: Deploy docs to website 33 | if: ${{ github.ref == 'refs/heads/trunk' && github.repository == 'JakeWharton/finalization-hook' }} 34 | uses: JamesIves/github-pages-deploy-action@releases/v3 35 | with: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | BRANCH: site 38 | FOLDER: build/dokka/html/ 39 | TARGET_FOLDER: docs/latest/ 40 | CLEAN: true 41 | -------------------------------------------------------------------------------- /.github/workflows/gradle-wrapper.yaml: -------------------------------------------------------------------------------- 1 | name: gradle-wrapper 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'gradlew' 7 | - 'gradlew.bat' 8 | - 'gradle/wrapper/**' 9 | 10 | jobs: 11 | validate: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: gradle/actions/wrapper-validation@v4 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '**' 7 | 8 | jobs: 9 | release: 10 | runs-on: macos-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-java@v4 15 | with: 16 | distribution: 'zulu' 17 | java-version-file: .github/workflows/.java-version 18 | 19 | - run: ./gradlew publish 20 | env: 21 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_CENTRAL_USERNAME }} 22 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_CENTRAL_PASSWORD }} 23 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_PRIVATE_KEY }} 24 | 25 | - name: Extract release notes 26 | id: release_notes 27 | uses: ffurrer2/extract-release-notes@v2 28 | 29 | - name: Create release 30 | uses: ncipollo/release-action@v1 31 | with: 32 | body: ${{ steps.release_notes.outputs.release_notes }} 33 | 34 | - run: ./gradlew dokkaHtml 35 | 36 | - name: Deploy docs to website 37 | uses: JamesIves/github-pages-deploy-action@releases/v3 38 | with: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | BRANCH: site 41 | FOLDER: build/dokka/html/ 42 | TARGET_FOLDER: docs/0.x/ 43 | CLEAN: true 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ IDEA 2 | /.idea 3 | 4 | # Gradle 5 | .gradle 6 | build 7 | /reports 8 | 9 | # Android 10 | local.properties 11 | 12 | # Kotlin 13 | .kotlin 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [Unreleased] 4 | [Unreleased]: https://github.com/cashapp/redwood/compare/0.1.0...HEAD 5 | 6 | New: 7 | - Nothing yet! 8 | 9 | Changed: 10 | - In-development snapshots are now published to the Central Portal Snapshots repository at https://central.sonatype.com/repository/maven-snapshots/. 11 | 12 | Fixed: 13 | - Nothing yet! 14 | 15 | 16 | ## [0.1.0] - 2024-08-30 17 | [0.1.0]: https://github.com/cashapp/redwood/releases/tag/0.1.0 18 | 19 | Initial release. 20 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Finalization Hook for Kotlin 2 | 3 | Run a "hook" lambda after a block of code regardless of whether the block 4 | succeeds, fails, or the application is killed. 5 | 6 | ```kotlin 7 | changeSomething() 8 | withFinalizationHook( 9 | hook = { resetSomething() }, 10 | block = { 11 | doWork() 12 | }, 13 | ) 14 | ``` 15 | 16 | This function should only be used when the changes being made are outside the application 17 | itself. An example would be writing a lock file to the file system, and then deleting it when 18 | an operation completes. If you are only changing state within the application, a regular 19 | `try`/`finally` will suffice. 20 | 21 | 22 | ## Test App 23 | 24 | Build the `:test-app` project to see this in action. Run the app and then hit 25 | Ctrl+C: 26 | 27 | ``` 28 | $ ./test-app/build/bin/macosArm64/releaseExecutable/test-app.kexe 29 | START 30 | BLOCK 31 | ^CHOOK 32 | ``` 33 | 34 | Or run the app and `kill` the PID: 35 | 36 | ``` 37 | $ ./test-app/build/bin/macosArm64/releaseExecutable/test-app.kexe 38 | START 39 | BLOCK 40 | HOOK 41 | Terminated: 15 42 | ``` 43 | (In another terminal pane) 44 | ``` 45 | $ ps | grep test-app 46 | 32769 ttys004 0:00.02 ./test-app/build/bin/macosArm64/releaseExecutable/test-app.kexe 47 | 32943 ttys005 0:00.00 grep test-app 48 | 49 | $ kill 32769 50 | ``` 51 | 52 | A JVM version is also available at `test-app/build/install/test-app/bin/test-app`. 53 | 54 | 55 | ## Download 56 | 57 | ```groovy 58 | dependencies { 59 | implementation("com.jakewharton.finalization:finalization-hook:0.1.0") 60 | } 61 | ``` 62 | 63 | Documentation is available at [jakewharton.github.io/finalization-hook/docs/0.x/](https://jakewharton.github.io/finalization-hook/docs/0.x/). 64 | 65 |
66 | Snapshots of the development version are available in the Central Portal Snapshots repository. 67 |

68 | 69 | ```groovy 70 | repository { 71 | mavenCentral() 72 | maven { 73 | url 'https://central.sonatype.com/repository/maven-snapshots/' 74 | } 75 | } 76 | dependencies { 77 | implementation("com.jakewharton.finalization:finalization-hook:0.2.0-SNAPSHOT") 78 | } 79 | ``` 80 | 81 | Snapshot documentation is available at [jakewharton.github.io/finalization-hook/docs/latest/](https://jakewharton.github.io/finalization-hook/docs/latest/). 82 | 83 |

84 |
85 | 86 | ## License 87 | 88 | Copyright 2024 Jake Wharton 89 | 90 | Licensed under the Apache License, Version 2.0 (the "License"); 91 | you may not use this file except in compliance with the License. 92 | You may obtain a copy of the License at 93 | 94 | http://www.apache.org/licenses/LICENSE-2.0 95 | 96 | Unless required by applicable law or agreed to in writing, software 97 | distributed under the License is distributed on an "AS IS" BASIS, 98 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 99 | See the License for the specific language governing permissions and 100 | limitations under the License. 101 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | 1. Update the `VERSION_NAME` in `gradle.properties` to the release version. 4 | 5 | 2. Update the `CHANGELOG.md`: 6 | 1. Change the `Unreleased` header to the release version. 7 | 2. Add a link URL to ensure the header link works. 8 | 3. Add a new `Unreleased` section to the top. 9 | 10 | 3. Update the `README.md`: 11 | 1. Change the "Download" section to reflect the new release version. 12 | 2. Change the snapshot section to reflect the next "SNAPSHOT" version, if it is changing. 13 | 14 | 4. Commit 15 | 16 | ``` 17 | $ git commit -am "Prepare version X.Y.X" 18 | ``` 19 | 20 | 5. Tag 21 | 22 | ``` 23 | $ git tag -am "Version X.Y.Z" X.Y.Z 24 | ``` 25 | 26 | 6. Update the `VERSION_NAME` in `gradle.properties` to the next "SNAPSHOT" version. 27 | 28 | 7. Commit 29 | 30 | ``` 31 | $ git commit -am "Prepare next development version" 32 | ``` 33 | 34 | 8. Push! 35 | 36 | ``` 37 | $ git push && git push --tags 38 | ``` 39 | 40 | This will trigger a GitHub Action workflow which will create a GitHub release and push to Maven Central. 41 | -------------------------------------------------------------------------------- /api/finalization-hook.api: -------------------------------------------------------------------------------- 1 | public final class com/jakewharton/finalization/Hook { 2 | public static final fun withFinalizationHook (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 3 | } 4 | 5 | -------------------------------------------------------------------------------- /api/finalization-hook.klib.api: -------------------------------------------------------------------------------- 1 | // Klib ABI Dump 2 | // Targets: [linuxArm64, linuxX64, macosArm64, macosX64, mingwX64] 3 | // Rendering settings: 4 | // - Signature version: 2 5 | // - Show manifest properties: true 6 | // - Show declarations: true 7 | 8 | // Library unique name: 9 | final suspend fun <#A: kotlin/Any?> com.jakewharton.finalization/withFinalizationHook(kotlin/Function0, kotlin.coroutines/SuspendFunction1): #A // com.jakewharton.finalization/withFinalizationHook|withFinalizationHook(kotlin.Function0;kotlin.coroutines.SuspendFunction1){0§}[0] 10 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget 2 | import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile 3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 4 | 5 | buildscript { 6 | dependencies { 7 | classpath libs.kotlin.plugin 8 | classpath libs.maven.publish.gradlePlugin 9 | classpath libs.dokka.gradlePlugin 10 | classpath libs.spotless.gradlePlugin 11 | classpath libs.binary.compatibility.validator.gradlePlugin 12 | } 13 | repositories { 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | apply plugin: 'org.jetbrains.kotlin.multiplatform' 20 | apply plugin: 'org.jetbrains.dokka' 21 | apply plugin: 'com.vanniktech.maven.publish' 22 | apply plugin: 'binary-compatibility-validator' 23 | 24 | kotlin { 25 | jvm() 26 | 27 | linuxArm64() 28 | linuxX64() 29 | 30 | macosArm64() 31 | macosX64() 32 | 33 | mingwX64() 34 | 35 | applyDefaultHierarchyTemplate { 36 | it.common { 37 | it.group("native") { 38 | it.group("posix") { 39 | it.group("linux") {} 40 | it.group("macos") {} 41 | } 42 | } 43 | } 44 | } 45 | 46 | explicitApi() 47 | 48 | sourceSets { 49 | commonMain { 50 | dependencies { 51 | api libs.kotlinx.coroutines.core 52 | } 53 | } 54 | commonTest { 55 | dependencies { 56 | implementation libs.kotlin.test 57 | implementation libs.kotlinx.coroutines.test 58 | implementation libs.assertk 59 | } 60 | } 61 | } 62 | 63 | compilerOptions.freeCompilerArgs.add('-Xexpect-actual-classes') 64 | } 65 | 66 | apiValidation { 67 | klib { 68 | enabled = true 69 | strictValidation = true 70 | } 71 | // Whhhyyy does this apply itself to all projects automatically?!? 72 | ignoredProjects += 'test-app' 73 | } 74 | 75 | group = GROUP 76 | version = VERSION_NAME 77 | 78 | allprojects { 79 | repositories { 80 | mavenCentral() 81 | } 82 | 83 | tasks.withType(JavaCompile).configureEach { 84 | sourceCompatibility = JavaVersion.VERSION_1_8 85 | targetCompatibility = JavaVersion.VERSION_1_8 86 | } 87 | 88 | tasks.withType(KotlinJvmCompile).configureEach { 89 | compilerOptions.jvmTarget = JvmTarget.JVM_1_8 90 | } 91 | 92 | apply plugin: 'com.diffplug.spotless' 93 | spotless { 94 | kotlin { 95 | target("src/**/*.kt") 96 | ktlint(libs.ktlint.core.get().version) 97 | .editorConfigOverride([ 98 | 'ktlint_standard_filename' : 'disabled', 99 | // Making something an expression body should be a choice around readability. 100 | 'ktlint_standard_function-expression-body': 'disabled', 101 | 'ktlint_standard_property-naming' : 'disabled', 102 | ]) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | GROUP=com.jakewharton.finalization 2 | VERSION_NAME=0.2.0-SNAPSHOT 3 | 4 | SONATYPE_HOST=CENTRAL_PORTAL 5 | SONATYPE_AUTOMATIC_RELEASE=true 6 | RELEASE_SIGNING_ENABLED=true 7 | 8 | POM_ARTIFACT_ID=finalization-hook 9 | POM_NAME=Finalization hook 10 | POM_DESCRIPTION=Run a "hook" lambda after a block of code regardless of whether the block succeeds, fails, or the application is killed 11 | 12 | POM_URL=https://github.com/JakeWharton/finalization-hook/ 13 | POM_SCM_URL=https://github.com/JakeWharton/finalization-hook/ 14 | POM_SCM_CONNECTION=scm:git:git://github.com/JakeWharton/finalization-hook.git 15 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/JakeWharton/finalization-hook.git 16 | 17 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 18 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 19 | POM_LICENCE_DIST=repo 20 | 21 | POM_DEVELOPER_ID=jakewharton 22 | POM_DEVELOPER_NAME=Jake Wharton 23 | POM_DEVELOPER_URL=https://github.com/JakeWharton/ 24 | 25 | kotlin.mpp.stability.nowarn=true 26 | kotlin.mpp.enableCInteropCommonization=true 27 | kotlin.native.ignoreDisabledTargets=true 28 | 29 | org.gradle.caching=true 30 | org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 31 | org.gradle.parallel=true 32 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.1.21" 3 | kotlinx-coroutines = "1.10.2" 4 | 5 | [libraries] 6 | kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 7 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } 8 | 9 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } 10 | kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } 11 | 12 | maven-publish-gradlePlugin = "com.vanniktech:gradle-maven-publish-plugin:0.32.0" 13 | dokka-gradlePlugin = "org.jetbrains.dokka:dokka-gradle-plugin:2.0.0" 14 | binary-compatibility-validator-gradlePlugin = "org.jetbrains.kotlinx:binary-compatibility-validator:0.17.0" 15 | spotless-gradlePlugin = "com.diffplug.spotless:spotless-plugin-gradle:7.0.4" 16 | ktlint-core = "com.pinterest.ktlint:ktlint-cli:1.6.0" 17 | 18 | assertk = "com.willowtreeapps.assertk:assertk:0.28.1" 19 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JakeWharton/finalization-hook/15ba15baec1ef25fbbddbeda6edad49f1d0569a2/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'finalization-hook' 2 | 3 | include ':' 4 | include ':test-app' 5 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/jakewharton/finalization/hook.kt: -------------------------------------------------------------------------------- 1 | package com.jakewharton.finalization 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | 5 | /** 6 | * Run [block], and then run [hook] regardless of whether the block 7 | * succeeds, fails, or the application is killed. 8 | * 9 | * [hook] is not guaranteed to run, but every effort will be made to have it run. 10 | * 11 | * The general pattern for using this function is: 12 | * ```kotlin 13 | * changeSomething() 14 | * withFinalizationHook( 15 | * hook = { resetSomething() }, 16 | * block = { 17 | * doWork() 18 | * }, 19 | * ) 20 | * ``` 21 | * 22 | * If a signal is received while [block] is executing, its coroutine will be cancelled, the [hook] 23 | * will be run, and then the signal will be redelivered to the application to terminate execution. 24 | * 25 | * This function should only be used when the changes being made are outside the application 26 | * itself. An example would be writing a lock file to the file system, and then deleting it when 27 | * an operation completes. If you are only changing state within the application, a regular 28 | * `try`/`finally` will suffice. 29 | */ 30 | public expect suspend fun withFinalizationHook( 31 | hook: () -> Unit, 32 | block: suspend CoroutineScope.() -> R, 33 | ): R 34 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/jakewharton/finalization/FinalizationHookTest.kt: -------------------------------------------------------------------------------- 1 | package com.jakewharton.finalization 2 | 3 | import assertk.assertFailure 4 | import assertk.assertThat 5 | import assertk.assertions.isSameInstanceAs 6 | import assertk.assertions.isTrue 7 | import kotlin.test.Test 8 | import kotlinx.coroutines.CoroutineStart.UNDISPATCHED 9 | import kotlinx.coroutines.awaitCancellation 10 | import kotlinx.coroutines.cancelAndJoin 11 | import kotlinx.coroutines.launch 12 | import kotlinx.coroutines.test.runTest 13 | 14 | class FinalizationHookTest { 15 | @Test fun callsInPlaceContract() = runTest { 16 | var success: Boolean 17 | withFinalizationHook( 18 | hook = { }, 19 | block = { success = true }, 20 | ) 21 | assertThat(success).isTrue() 22 | } 23 | 24 | @Test fun blockStartedWithoutSuspension() = runTest { 25 | var started = false 26 | backgroundScope.launch(start = UNDISPATCHED) { 27 | withFinalizationHook( 28 | hook = { }, 29 | block = { 30 | started = true 31 | awaitCancellation() 32 | }, 33 | ) 34 | } 35 | assertThat(started).isTrue() 36 | } 37 | 38 | @Test fun hookRunsOnNormalCompletion() = runTest { 39 | var ran = false 40 | val value = Any() 41 | val result = withFinalizationHook( 42 | hook = { ran = true }, 43 | block = { value }, 44 | ) 45 | assertThat(ran).isTrue() 46 | assertThat(result).isSameInstanceAs(value) 47 | } 48 | 49 | @Test fun hookRunsOnException() = runTest { 50 | var ran = false 51 | val exception = MyException() 52 | assertFailure { 53 | withFinalizationHook( 54 | hook = { ran = true }, 55 | block = { 56 | throw exception 57 | }, 58 | ) 59 | }.isSameInstanceAs(exception) 60 | assertThat(ran).isTrue() 61 | } 62 | 63 | @Test fun hookRunsOnCancellation() = runTest { 64 | var ran = false 65 | val job = launch(start = UNDISPATCHED) { 66 | withFinalizationHook( 67 | hook = { ran = true }, 68 | block = { awaitCancellation() }, 69 | ) 70 | } 71 | job.cancelAndJoin() 72 | assertThat(ran).isTrue() 73 | } 74 | 75 | private class MyException : RuntimeException() 76 | } 77 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/com/jakewharton/finalization/hook.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("Hook") 2 | 3 | package com.jakewharton.finalization 4 | 5 | import java.util.concurrent.atomic.AtomicBoolean 6 | import kotlin.contracts.ExperimentalContracts 7 | import kotlin.contracts.InvocationKind.EXACTLY_ONCE 8 | import kotlin.contracts.contract 9 | import kotlinx.coroutines.CoroutineScope 10 | import kotlinx.coroutines.coroutineScope 11 | 12 | @OptIn(ExperimentalContracts::class) 13 | public actual suspend fun withFinalizationHook( 14 | hook: () -> Unit, 15 | block: suspend CoroutineScope.() -> R, 16 | ): R { 17 | contract { 18 | callsInPlace(block, EXACTLY_ONCE) 19 | } 20 | 21 | val tryRunHook = object : AtomicBoolean(false), Runnable { 22 | override fun run() { 23 | if (compareAndSet(false, true)) { 24 | hook() 25 | } 26 | } 27 | } 28 | 29 | val runtime = Runtime.getRuntime() 30 | val hookThread = Thread(tryRunHook) 31 | runtime.addShutdownHook(hookThread) 32 | 33 | return try { 34 | coroutineScope(block) 35 | } finally { 36 | tryRunHook.run() 37 | runtime.removeShutdownHook(hookThread) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/mingwMain/kotlin/com/jakewharton/finalization/hook.kt: -------------------------------------------------------------------------------- 1 | package com.jakewharton.finalization 2 | 3 | import kotlinx.cinterop.ExperimentalForeignApi 4 | import kotlinx.cinterop.staticCFunction 5 | import platform.windows.DWORD 6 | import platform.windows.FALSE 7 | import platform.windows.SetConsoleCtrlHandler 8 | import platform.windows.TRUE 9 | import platform.windows.WINBOOL 10 | 11 | @OptIn(ExperimentalForeignApi::class) 12 | internal actual fun installAllSignalHandlers() { 13 | val signalHandlerFunction = staticCFunction(::signalHandler) 14 | SetConsoleCtrlHandler(signalHandlerFunction, TRUE) != 0 15 | } 16 | 17 | @OptIn(ExperimentalForeignApi::class) 18 | internal actual fun restoreSignalHandlerAndTerminate(signal: Int): Boolean { 19 | return SetConsoleCtrlHandler(null, FALSE) != 0 20 | } 21 | 22 | @OptIn(ExperimentalForeignApi::class) 23 | internal actual fun clearAllSignalHandlers() { 24 | SetConsoleCtrlHandler(null, FALSE) 25 | } 26 | 27 | private fun signalHandler(value: DWORD): WINBOOL { 28 | val signalHandler = signalHandlerRef.value 29 | if (signalHandler != null) { 30 | signalHandler.invoke(value.toInt()) 31 | // Prevent the default handler from being called. 32 | return TRUE 33 | } 34 | // It is not possible for the signal handler lambda to be null. Juuuuuust in case, 35 | // allow the default behavior run which should terminate the process. 36 | return FALSE 37 | } 38 | -------------------------------------------------------------------------------- /src/nativeMain/kotlin/com/jakewharton/finalization/hook.kt: -------------------------------------------------------------------------------- 1 | package com.jakewharton.finalization 2 | 3 | import kotlin.concurrent.AtomicInt 4 | import kotlin.concurrent.AtomicReference 5 | import kotlin.contracts.ExperimentalContracts 6 | import kotlin.contracts.InvocationKind.EXACTLY_ONCE 7 | import kotlin.contracts.contract 8 | import kotlin.coroutines.coroutineContext 9 | import kotlin.system.exitProcess 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.Job 12 | import kotlinx.coroutines.job 13 | import kotlinx.coroutines.withContext 14 | import platform.posix.sleep 15 | 16 | internal val signalHandlerRef = AtomicReference<((Int) -> Unit)?>(null) 17 | 18 | internal expect fun installAllSignalHandlers() 19 | internal expect fun restoreSignalHandlerAndTerminate(signal: Int): Boolean 20 | internal expect fun clearAllSignalHandlers() 21 | 22 | @OptIn(ExperimentalContracts::class) 23 | public actual suspend fun withFinalizationHook( 24 | hook: () -> Unit, 25 | block: suspend CoroutineScope.() -> R, 26 | ): R { 27 | contract { 28 | callsInPlace(block, EXACTLY_ONCE) 29 | } 30 | 31 | val job = Job(coroutineContext.job) 32 | val signalRef = AtomicInt(0) 33 | val signalHandler: (Int) -> Unit = { signal -> 34 | // It's possible multiple signals are received before we can clear this callback. 35 | // The code below only does a single read of this value, so the last received will win. 36 | signalRef.value = signal 37 | // Force the code below to jump directly into the 'finally' block. 38 | job.cancel() 39 | } 40 | check(signalHandlerRef.compareAndSet(null, signalHandler)) { 41 | "Cannot nest multiple shutdown hooks" 42 | } 43 | 44 | installAllSignalHandlers() 45 | 46 | return try { 47 | withContext(job, block) 48 | } finally { 49 | // Required so that the caller's scope can exit normally. 50 | job.complete() 51 | 52 | hook() 53 | 54 | val signalValue = signalRef.value 55 | if (signalValue != 0) { 56 | if (restoreSignalHandlerAndTerminate(signalValue)) { 57 | // Since signal handling is asynchronous, sleep in the hopes we are 58 | // killed before it returns. 59 | sleep(1U) 60 | } 61 | 62 | // If we fail to restore the default signal handler, fail to kill ourselves, 63 | // or fail to be terminated in a reasonable amount of time, fallback to an exit. 64 | exitProcess(1) 65 | } 66 | 67 | clearAllSignalHandlers() 68 | signalHandlerRef.value = null 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/posixMain/kotlin/com/jakewharton/finalization/hook.kt: -------------------------------------------------------------------------------- 1 | package com.jakewharton.finalization 2 | 3 | import kotlinx.cinterop.ExperimentalForeignApi 4 | import kotlinx.cinterop.staticCFunction 5 | import platform.posix.SIGABRT 6 | import platform.posix.SIGALRM 7 | import platform.posix.SIGBUS 8 | import platform.posix.SIGFPE 9 | import platform.posix.SIGHUP 10 | import platform.posix.SIGINT 11 | import platform.posix.SIGQUIT 12 | import platform.posix.SIGTERM 13 | import platform.posix.SIG_DFL 14 | import platform.posix.SIG_ERR 15 | import platform.posix.kill 16 | import platform.posix.signal 17 | 18 | private val allSignals = arrayOf(SIGABRT, SIGALRM, SIGBUS, SIGFPE, SIGHUP, SIGINT, SIGTERM, SIGQUIT) 19 | 20 | @OptIn(ExperimentalForeignApi::class) 21 | internal actual fun installAllSignalHandlers() { 22 | val signalHandlerFunction = staticCFunction(::signalHandler) 23 | for (signal in allSignals) { 24 | // TODO Migrate to sigaction. 25 | signal(signal, signalHandlerFunction) 26 | } 27 | } 28 | 29 | @OptIn(ExperimentalForeignApi::class) 30 | internal actual fun restoreSignalHandlerAndTerminate(signal: Int): Boolean { 31 | return signal(signal, SIG_DFL) != SIG_ERR && kill(0, signal) == 0 32 | } 33 | 34 | @OptIn(ExperimentalForeignApi::class) 35 | internal actual fun clearAllSignalHandlers() { 36 | for (signal in allSignals) { 37 | signal(signal, SIG_DFL) 38 | } 39 | } 40 | 41 | @OptIn(ExperimentalForeignApi::class) 42 | private fun signalHandler(value: Int) { 43 | val signalHandler = signalHandlerRef.value 44 | if (signalHandler != null) { 45 | signalHandler.invoke(value) 46 | } else { 47 | // It is not possible for the signal handler lambda to be null. Juuuuuust in case, 48 | // remove ourselves as the signal handler and re-send it to produce the default behavior. 49 | signal(value, SIG_DFL) 50 | kill(0, value) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test-app/build.gradle: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget 2 | import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType 3 | 4 | apply plugin: 'org.jetbrains.kotlin.multiplatform' 5 | 6 | kotlin { 7 | jvm { 8 | binaries { 9 | executable { 10 | mainClass = 'example.Main' 11 | } 12 | } 13 | } 14 | 15 | linuxArm64() 16 | linuxX64() 17 | 18 | macosArm64() 19 | macosX64() 20 | 21 | mingwX64() 22 | 23 | sourceSets { 24 | commonMain { 25 | dependencies { 26 | implementation project(':') 27 | } 28 | } 29 | } 30 | 31 | targets.withType(KotlinNativeTarget).configureEach { target -> 32 | target.binaries.executable { 33 | entryPoint = 'example.main' 34 | } 35 | target.binaries.configureEach { 36 | if (it.buildType == NativeBuildType.DEBUG) { 37 | it.linkTaskProvider.configure { 38 | enabled = false 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test-app/src/commonMain/kotlin/example/main.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("Main") 2 | 3 | package example 4 | 5 | import com.jakewharton.finalization.withFinalizationHook 6 | import kotlin.jvm.JvmName 7 | import kotlin.time.Duration.Companion.seconds 8 | import kotlinx.coroutines.delay 9 | import kotlinx.coroutines.runBlocking 10 | 11 | fun main() = runBlocking { 12 | println("START") 13 | withFinalizationHook( 14 | hook = { println("HOOK") }, 15 | block = { 16 | println("BLOCK") 17 | delay(30.seconds) 18 | }, 19 | ) 20 | println("END") 21 | } 22 | --------------------------------------------------------------------------------