diff --git a/Cargo.lock b/Cargo.lock index 0a70412a..48aa0ce2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,6 +99,89 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "async-broadcast" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b19760fa2b7301cf235360ffd6d3558b1ed4249edd16d6cca8d690cee265b95" +dependencies = [ + "event-listener", + "futures-core", + "parking_lot 0.12.1", +] + +[[package]] +name = "async-executor" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" +dependencies = [ + "async-lock", + "autocfg", + "concurrent-queue", + "futures-lite", + "libc", + "log", + "parking", + "polling", + "slab", + "socket2", + "waker-fn", + "windows-sys 0.42.0", +] + +[[package]] +name = "async-lock" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" +dependencies = [ + "event-listener", + "futures-lite", +] + +[[package]] +name = "async-recursion" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-task" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" + +[[package]] +name = "async-trait" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atk" version = "0.15.1" @@ -511,6 +594,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "const-oid" version = "0.7.1" @@ -820,6 +912,17 @@ dependencies = [ "pem-rfc7468", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -972,6 +1075,27 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "enumflags2" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -1174,6 +1298,21 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + [[package]] name = "futures-macro" version = "0.3.25" @@ -2365,6 +2504,20 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags", + "cfg-if 1.0.0", + "libc", + "memoffset", + "pin-utils", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -2595,6 +2748,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-stream" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4eb9ba3f3e42dbdd3b7b122de5ca169c81e93d561eb900da3a8c99bcfcf381a" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "overload" version = "0.1.1" @@ -2626,6 +2789,12 @@ dependencies = [ "system-deps 6.0.3", ] +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + [[package]] name = "parking_lot" version = "0.11.2" @@ -2898,6 +3067,20 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22122d5ec4f9fe1b3916419b76be1e80bcb93f618d071d2edf841b137b2a2bd6" +dependencies = [ + "autocfg", + "cfg-if 1.0.0", + "libc", + "log", + "wepoll-ffi", + "windows-sys 0.42.0", +] + [[package]] name = "poly1305" version = "0.7.2" @@ -3793,6 +3976,12 @@ dependencies = [ "loom", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "string_cache" version = "0.8.4" @@ -4205,6 +4394,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "tauri-plugin-single-instance" +version = "0.1.0" +dependencies = [ + "log", + "serde", + "serde_json", + "tauri", + "thiserror", + "windows-sys 0.42.0", + "zbus", +] + [[package]] name = "tauri-plugin-sql" version = "0.1.0" @@ -4711,6 +4913,16 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" +[[package]] +name = "uds_windows" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +dependencies = [ + "tempfile", + "winapi 0.3.9", +] + [[package]] name = "unicode-bidi" version = "0.3.8" @@ -4860,6 +5072,12 @@ dependencies = [ "libc", ] +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + [[package]] name = "walkdir" version = "2.3.2" @@ -5079,6 +5297,15 @@ dependencies = [ "windows-metadata", ] +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + [[package]] name = "whoami" version = "1.2.3" @@ -5413,6 +5640,68 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" +[[package]] +name = "zbus" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "379d587c0ccb632d1179cf44082653f682842f0535f0fdfaefffc34849cc855e" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-recursion", + "async-task", + "async-trait", + "byteorder", + "derivative", + "dirs", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "once_cell", + "ordered-stream", + "rand 0.8.5", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "winapi 0.3.9", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66492a2e90c0df7190583eccb8424aa12eb7ff06edea415a4fff6688fae18cf8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "zbus_names" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f34f314916bd89bdb9934154627fab152f4f28acdda03e7c4c68181b214fe7e3" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + [[package]] name = "zeroize" version = "1.5.7" @@ -5433,3 +5722,29 @@ dependencies = [ "syn", "synstructure", ] + +[[package]] +name = "zvariant" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576cc41e65c7f283e5460f5818073e68fb1f1631502b969ef228c2e03c862efb" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fd4aafc0dee96ae7242a24249ce9babf21e1562822f03df650d4e68c20e41ed" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] diff --git a/plugins/single-instance/Cargo.toml b/plugins/single-instance/Cargo.toml new file mode 100644 index 00000000..b6fc1cbe --- /dev/null +++ b/plugins/single-instance/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "tauri-plugin-single-instance" +version = "0.1.0" +description = "Ensure a single instance of your tauri app is running." +authors.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde.workspace = true +serde_json.workspace = true +tauri.workspace = true +log.workspace = true +thiserror.workspace = true + +[target.'cfg(target_os = "windows")'.dependencies.windows-sys] +version = "0.42" +features = [ + "Win32_System_Threading", + "Win32_System_DataExchange", + "Win32_Foundation", + "Win32_UI_WindowsAndMessaging", + "Win32_Security", + "Win32_System_LibraryLoader", + "Win32_Graphics_Gdi", +] + +[target.'cfg(target_os = "linux")'.dependencies] +zbus = "3.7" \ No newline at end of file diff --git a/plugins/single-instance/LICENSE.spdx b/plugins/single-instance/LICENSE.spdx new file mode 100644 index 00000000..cdd0df5a --- /dev/null +++ b/plugins/single-instance/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/single-instance/LICENSE_APACHE-2.0 b/plugins/single-instance/LICENSE_APACHE-2.0 new file mode 100644 index 00000000..4947287f --- /dev/null +++ b/plugins/single-instance/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/plugins/single-instance/LICENSE_MIT b/plugins/single-instance/LICENSE_MIT new file mode 100644 index 00000000..4d754725 --- /dev/null +++ b/plugins/single-instance/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/plugins/single-instance/README.md b/plugins/single-instance/README.md new file mode 100644 index 00000000..5823ab98 --- /dev/null +++ b/plugins/single-instance/README.md @@ -0,0 +1,61 @@ +![{{plugin name}}](banner.jpg) + + + +## Install + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` +```toml +[dependencies] + = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" } +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + +> Note: Since most JavaScript package managers are unable to install packages from git monorepos we provide read-only mirrors of each plugin. This makes installation option 2 more ergonomic to use. + +```sh +pnpm add +# or +npm add +# or +yarn add +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/main.rs` +```rust +fn main() { + tauri::Builder::default() + .plugin() + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript + +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/single-instance/src/lib.rs b/plugins/single-instance/src/lib.rs new file mode 100644 index 00000000..a2ff71af --- /dev/null +++ b/plugins/single-instance/src/lib.rs @@ -0,0 +1,24 @@ +use tauri::{plugin::TauriPlugin, AppHandle, Manager, Runtime}; + +#[cfg(target_os = "windows")] +#[path = "platform_impl/windows.rs"] +mod platform_impl; +#[cfg(target_os = "linux")] +#[path = "platform_impl/linux.rs"] +mod platform_impl; +#[cfg(target_os = "macos")] +#[path = "platform_impl/macos.rs"] +mod platform_impl; + +pub(crate) type SingleInstanceCallback = + dyn FnMut(&AppHandle, Vec, String) + Send + Sync + 'static; + +pub fn init, Vec, String) + Send + Sync + 'static>( + f: F, +) -> TauriPlugin { + platform_impl::init(Box::new(f)) +} + +pub fn destroy>(manager: &M) { + platform_impl::destroy(manager) +} diff --git a/plugins/single-instance/src/platform_impl/linux.rs b/plugins/single-instance/src/platform_impl/linux.rs new file mode 100644 index 00000000..b175d727 --- /dev/null +++ b/plugins/single-instance/src/platform_impl/linux.rs @@ -0,0 +1,96 @@ +#![cfg(target_os = "linux")] + +use std::sync::Arc; + +use crate::SingleInstanceCallback; +use tauri::{ + plugin::{self, TauriPlugin}, + AppHandle, Config, Manager, RunEvent, Runtime, +}; +use zbus::{ + blocking::{Connection, ConnectionBuilder}, + dbus_interface, +}; + +struct ConnectionHandle(Connection); + +struct SingleInstanceDBus { + callback: Box>, + app_handle: AppHandle, +} + +#[dbus_interface(name = "org.SingleInstance.DBus")] +impl SingleInstanceDBus { + fn execute_callback(&mut self, argv: Vec, cwd: String) { + (self.callback)(&self.app_handle, argv, cwd); + } +} + +fn dbus_id(config: Arc) -> String { + config + .tauri + .bundle + .identifier + .replace('.', "_") + .replace('-', "_") +} + +pub fn init(f: Box>) -> TauriPlugin { + plugin::Builder::new("single-instance") + .setup(|app| { + let id = dbus_id(app.config()); + let single_instance_dbus = SingleInstanceDBus { + callback: f, + app_handle: app.clone(), + }; + let dbus_name = format!("org.{}.SingleInstance", id); + let dbus_path = format!("/org/{}/SingleInstance", id); + + match ConnectionBuilder::session() + .unwrap() + .name(dbus_name.as_str()) + .unwrap() + .serve_at(dbus_path.as_str(), single_instance_dbus) + .unwrap() + .build() + { + Ok(connection) => { + app.manage(ConnectionHandle(connection)); + } + Err(zbus::Error::NameTaken) => { + if let Ok(connection) = Connection::session() { + let _ = connection.call_method( + Some(dbus_name.as_str()), + dbus_path.as_str(), + Some("org.SingleInstance.DBus"), + "ExecuteCallback", + &( + std::env::args().collect::>(), + std::env::current_dir() + .unwrap_or_default() + .to_str() + .unwrap_or_default(), + ), + ); + } + std::process::exit(0) + } + _ => {} + } + + Ok(()) + }) + .on_event(|app, event| { + if let RunEvent::Exit = event { + destroy(app); + } + }) + .build() +} + +pub fn destroy>(manager: &M) { + if let Some(connection) = manager.try_state::() { + let dbus_name = format!("org.{}.SingleInstance", dbus_id(manager.config())); + let _ = connection.0.release_name(dbus_name); + } +} diff --git a/plugins/single-instance/src/platform_impl/macos.rs b/plugins/single-instance/src/platform_impl/macos.rs new file mode 100644 index 00000000..1b408c66 --- /dev/null +++ b/plugins/single-instance/src/platform_impl/macos.rs @@ -0,0 +1,12 @@ +#![cfg(target_os = "macos")] + +use crate::SingleInstanceCallback; +use tauri::{ + plugin::{self, TauriPlugin}, + Manager, Runtime, +}; +pub fn init(_f: Box>) -> TauriPlugin { + plugin::Builder::new("single-instance").build() +} + +pub fn destroy>(_manager: &M) {} diff --git a/plugins/single-instance/src/platform_impl/windows.rs b/plugins/single-instance/src/platform_impl/windows.rs new file mode 100644 index 00000000..a4eadab4 --- /dev/null +++ b/plugins/single-instance/src/platform_impl/windows.rs @@ -0,0 +1,215 @@ +#![cfg(target_os = "windows")] + +use crate::SingleInstanceCallback; +use std::ffi::CStr; +use tauri::{ + plugin::{self, TauriPlugin}, + AppHandle, Manager, RunEvent, Runtime, +}; +use windows_sys::Win32::{ + Foundation::{CloseHandle, GetLastError, ERROR_ALREADY_EXISTS, HWND, LPARAM, LRESULT, WPARAM}, + System::{ + DataExchange::COPYDATASTRUCT, + LibraryLoader::GetModuleHandleW, + Threading::{CreateMutexW, ReleaseMutex}, + }, + UI::WindowsAndMessaging::{ + self as w32wm, CreateWindowExW, DefWindowProcW, DestroyWindow, FindWindowW, + RegisterClassExW, SendMessageW, GWL_STYLE, GWL_USERDATA, WINDOW_LONG_PTR_INDEX, + WM_COPYDATA, WM_DESTROY, WNDCLASSEXW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, + WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_VISIBLE, + }, +}; + +struct MutexHandle(isize); +struct TargetWindowHandle(isize); + +const WMCOPYDATA_SINGLE_INSTANCE_DATA: usize = 1542; + +pub fn init(f: Box>) -> TauriPlugin { + plugin::Builder::new("single-instance") + .setup(|app| { + let id = &app.config().tauri.bundle.identifier; + + let class_name = encode_wide(format!("{}-sic", id)); + let window_name = encode_wide(format!("{}-siw", id)); + let mutex_name = encode_wide(format!("{}-sim", id)); + + let hmutex = + unsafe { CreateMutexW(std::ptr::null(), true.into(), mutex_name.as_ptr()) }; + + if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS { + unsafe { + let hwnd = FindWindowW(class_name.as_ptr(), window_name.as_ptr()); + + if hwnd != 0 { + let data = format!( + "{}|{}\0", + std::env::current_dir() + .unwrap_or_default() + .to_str() + .unwrap_or_default(), + std::env::args().collect::>().join("|") + ); + let bytes = data.as_bytes(); + let cds = COPYDATASTRUCT { + dwData: WMCOPYDATA_SINGLE_INSTANCE_DATA, + cbData: bytes.len() as _, + lpData: bytes.as_ptr() as _, + }; + SendMessageW(hwnd, WM_COPYDATA, 0, &cds as *const _ as _); + app.exit(0); + } + } + } else { + app.manage(MutexHandle(hmutex)); + + let hwnd = create_event_target_window::(&class_name, &window_name); + unsafe { + SetWindowLongPtrW( + hwnd, + GWL_USERDATA, + Box::into_raw(Box::new((app.clone(), f))) as _, + ) + }; + + app.manage(TargetWindowHandle(hwnd)); + } + + Ok(()) + }) + .on_event(|app, event| { + if let RunEvent::Exit = event { + destroy(app); + } + }) + .build() +} + +pub fn destroy>(manager: &M) { + if let Some(hmutex) = manager.try_state::() { + unsafe { + ReleaseMutex(hmutex.0); + CloseHandle(hmutex.0); + } + } + if let Some(hwnd) = manager.try_state::() { + unsafe { DestroyWindow(hwnd.0) }; + } +} + +unsafe extern "system" fn single_instance_window_proc( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + let data_ptr = GetWindowLongPtrW(hwnd, GWL_USERDATA) + as *mut (AppHandle, Box>); + let (app_handle, callback) = &mut *data_ptr; + + match msg { + WM_COPYDATA => { + let cds_ptr = lparam as *const COPYDATASTRUCT; + if (*cds_ptr).dwData == WMCOPYDATA_SINGLE_INSTANCE_DATA { + let data = CStr::from_ptr((*cds_ptr).lpData as _).to_string_lossy(); + let mut s = data.split("|"); + let cwd = s.next().unwrap(); + let args = s.into_iter().map(|s| s.to_string()).collect(); + callback(&app_handle, args, cwd.to_string()); + } + 1 + } + + WM_DESTROY => { + let _ = Box::from_raw(data_ptr); + 0 + } + _ => DefWindowProcW(hwnd, msg, wparam, lparam), + } +} + +fn create_event_target_window(class_name: &[u16], window_name: &[u16]) -> HWND { + unsafe { + let class = WNDCLASSEXW { + cbSize: std::mem::size_of::() as u32, + style: 0, + lpfnWndProc: Some(single_instance_window_proc::), + cbClsExtra: 0, + cbWndExtra: 0, + hInstance: GetModuleHandleW(std::ptr::null()), + hIcon: 0, + hCursor: 0, + hbrBackground: 0, + lpszMenuName: std::ptr::null(), + lpszClassName: class_name.as_ptr(), + hIconSm: 0, + }; + + RegisterClassExW(&class); + + let hwnd = CreateWindowExW( + WS_EX_NOACTIVATE + | WS_EX_TRANSPARENT + | WS_EX_LAYERED + // WS_EX_TOOLWINDOW prevents this window from ever showing up in the taskbar, which + // we want to avoid. If you remove this style, this window won't show up in the + // taskbar *initially*, but it can show up at some later point. This can sometimes + // happen on its own after several hours have passed, although this has proven + // difficult to reproduce. Alternatively, it can be manually triggered by killing + // `explorer.exe` and then starting the process back up. + // It is unclear why the bug is triggered by waiting for several hours. + | WS_EX_TOOLWINDOW, + class_name.as_ptr(), + window_name.as_ptr(), + WS_OVERLAPPED, + 0, + 0, + 0, + 0, + 0, + 0, + GetModuleHandleW(std::ptr::null()), + std::ptr::null(), + ); + SetWindowLongPtrW( + hwnd, + GWL_STYLE, + // The window technically has to be visible to receive WM_PAINT messages (which are used + // for delivering events during resizes), but it isn't displayed to the user because of + // the LAYERED style. + (WS_VISIBLE | WS_POPUP) as isize, + ); + hwnd + } +} + +pub fn encode_wide(string: impl AsRef) -> Vec { + std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref()) + .chain(std::iter::once(0)) + .collect() +} + +#[cfg(target_pointer_width = "32")] +#[allow(non_snake_case)] +unsafe fn SetWindowLongPtrW(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX, value: isize) -> isize { + w32wm::SetWindowLongW(hwnd, index, value as _) as _ +} + +#[cfg(target_pointer_width = "64")] +#[allow(non_snake_case)] +unsafe fn SetWindowLongPtrW(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX, value: isize) -> isize { + w32wm::SetWindowLongPtrW(hwnd, index, value) +} + +#[cfg(target_pointer_width = "32")] +#[allow(non_snake_case)] +unsafe fn GetWindowLongPtrW(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX) -> isize { + w32wm::GetWindowLongW(hwnd, index) as _ +} + +#[cfg(target_pointer_width = "64")] +#[allow(non_snake_case)] +unsafe fn GetWindowLongPtrW(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX) -> isize { + w32wm::GetWindowLongPtrW(hwnd, index) +}