Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.margelo.nitro.cactus

import android.util.Log
import com.margelo.nitro.NitroModules
import com.margelo.nitro.core.Promise
import java.io.BufferedInputStream
Expand All @@ -15,6 +16,11 @@ import kotlin.math.floor

class HybridCactusFileSystem : HybridCactusFileSystemSpec() {
private val context = NitroModules.applicationContext ?: error("Android context not found")

@Volatile
private var activeConnection: HttpURLConnection? = null
@Volatile
private var isCancelled: Boolean = false

override fun getCactusDirectory(): Promise<String> = Promise.async { cactusFile().absolutePath }

Expand Down Expand Up @@ -86,6 +92,7 @@ class HybridCactusFileSystem : HybridCactusFileSystemSpec() {

val tmpZip = File.createTempFile("dl_", ".zip", context.cacheDir)
var connection: HttpURLConnection? = null
isCancelled = false

try {
connection =
Expand All @@ -94,6 +101,7 @@ class HybridCactusFileSystem : HybridCactusFileSystemSpec() {
readTimeout = 5 * 60_000
instanceFollowRedirects = true
}
activeConnection = connection
connection.connect()
val code = connection.responseCode

Expand All @@ -115,6 +123,10 @@ class HybridCactusFileSystem : HybridCactusFileSystemSpec() {
val buf = ByteArray(256 * 1024)

while (true) {
if (isCancelled) {
throw InterruptedException("Download cancelled")
}

val read = bis.read(buf)

if (read == -1) {
Expand Down Expand Up @@ -155,6 +167,17 @@ class HybridCactusFileSystem : HybridCactusFileSystemSpec() {
} finally {
tmpZip.delete()
connection?.disconnect()
activeConnection = null
}
}
}

override fun stopDownload(model: String): Promise<Unit> {
return Promise.async {
if (activeConnection != null) {
isCancelled = true
activeConnection?.disconnect()
activeConnection = null
Comment on lines +175 to +180
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition: The activeConnection and isCancelled variables are marked @Volatile but are not properly synchronized. The check-then-act pattern in stopDownload (lines 177-180) is not atomic. Between checking activeConnection != null and calling disconnect(), another thread could set it to null. Consider using synchronized blocks or a lock to ensure thread safety.

Copilot uses AI. Check for mistakes.
}
}
}
Expand Down
11 changes: 7 additions & 4 deletions cpp/HybridCactusUtil.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,17 @@ HybridCactusUtil::registerApp(const std::string &encryptedData) {
});
}

std::shared_ptr<Promise<std::optional<std::string>>>
std::shared_ptr<Promise<std::variant<nitro::NullType, std::string>>>
HybridCactusUtil::getDeviceId() {
return Promise<std::optional<std::string>>::async(
[this]() -> std::optional<std::string> {
return Promise<std::variant<nitro::NullType, std::string>>::async(
[this]() -> std::variant<nitro::NullType, std::string> {
std::lock_guard<std::mutex> lock(this->_mutex);

const char *deviceId = get_device_id();
return deviceId ? std::optional<std::string>(deviceId) : std::nullopt;
if (deviceId) {
return std::string(deviceId);
}
return nitro::NullType();
});
}

Expand Down
4 changes: 3 additions & 1 deletion cpp/HybridCactusUtil.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#include "cactus_util.h"

#include <mutex>
#include <variant>
#include <NitroModules/Null.hpp>

namespace margelo::nitro::cactus {

Expand All @@ -14,7 +16,7 @@ class HybridCactusUtil : public HybridCactusUtilSpec {
std::shared_ptr<Promise<std::string>>
registerApp(const std::string &encryptedData) override;

std::shared_ptr<Promise<std::optional<std::string>>> getDeviceId() override;
std::shared_ptr<Promise<std::variant<nitro::NullType, std::string>>> getDeviceId() override;

std::shared_ptr<Promise<void>>
setAndroidDataDirectory(const std::string &dataDir) override;
Expand Down
10 changes: 5 additions & 5 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ PODS:
- hermes-engine (0.81.1):
- hermes-engine/Pre-built (= 0.81.1)
- hermes-engine/Pre-built (0.81.1)
- NitroModules (0.31.4):
- NitroModules (0.31.10):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -1808,7 +1808,7 @@ PODS:
- React-RCTFBReactNativeSpec
- ReactCommon/turbomodule/core
- SocketRocket
- react-native-document-picker (11.0.0):
- react-native-document-picker (11.0.2):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -2650,8 +2650,8 @@ SPEC CHECKSUMS:
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
hermes-engine: 4f8246b1f6d79f625e0d99472d1f3a71da4d28ca
NitroModules: b2a738b643842e817d7bb8a3b157b5223a92a2fa
RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f
NitroModules: 22837572f1ecf8f1560f5aac7c9f687825754901
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
RCTDeprecation: c4b9e2fd0ab200e3af72b013ed6113187c607077
RCTRequired: e97dd5dafc1db8094e63bc5031e0371f092ae92a
RCTTypeSafety: 720403058b7c1380c6a3ae5706981d6362962c89
Expand Down Expand Up @@ -2684,7 +2684,7 @@ SPEC CHECKSUMS:
React-logger: d27dd2000f520bf891d24f6e141cde34df41f0ee
React-Mapbuffer: 0746ffab5ac0f49b7c9347338e3d0c1d9dd634c8
React-microtasksnativemodule: b0fb3f97372df39bda3e657536039f1af227cc29
react-native-document-picker: 63639c144fbdc4bf7b12d31a3827ae2bfbaf7ad4
react-native-document-picker: db7c999307fb2e15db62ac79bf61c1fc47bba230
react-native-image-picker: 43e6cd4231e670030fe09b079d696fa5a634ccfc
React-NativeModulesApple: 9ec9240159974c94886ebbe4caec18e3395f6aef
React-oscompat: b12c633e9c00f1f99467b1e0e0b8038895dae436
Expand Down
1 change: 1 addition & 0 deletions example/src/PerformanceScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const PerformanceScreen = () => {

// Cleanup on unmount
return () => {
cactusLM.stopDownload().catch(() => {});
cactusLM.destroy();
};
}, []);
Expand Down
16 changes: 14 additions & 2 deletions ios/HybridCactusFileSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,12 @@ class HybridCactusFileSystem: HybridCactusFileSystemSpec {

let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
let task = session.downloadTask(with: url)

self.activeTask = task
callback?(0.0)

let (fileURL, response) = try await delegate.awaitCompletion(for: task)

self.activeTask = nil

guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode)
Expand All @@ -121,6 +123,17 @@ class HybridCactusFileSystem: HybridCactusFileSystemSpec {
}
}
}

private var activeTask: URLSessionDownloadTask?
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition: The activeTask property is not thread-safe. Multiple concurrent calls to downloadModel could overwrite each other's tasks, and stopDownload could cancel the wrong download. Consider using a dictionary keyed by model name to track multiple downloads, or add synchronization to prevent concurrent downloads of the same model.

Copilot uses AI. Check for mistakes.

func stopDownload(model: String) throws -> Promise<Void> {
return Promise.async {
if let task = self.activeTask {
task.cancel()
self.activeTask = nil
}
}
}

func deleteModel(model: String) throws -> Promise<Void> {
return Promise.async {
Expand All @@ -145,7 +158,6 @@ class HybridCactusFileSystem: HybridCactusFileSystemSpec {
if !FileManager.default.fileExists(atPath: cactusURL.path) {
try FileManager.default.createDirectory(at: cactusURL, withIntermediateDirectories: true)

// Exclude from iCloud backup
var resourceValues = URLResourceValues()
resourceValues.isExcludedFromBackup = true
try cactusURL.setResourceValues(resourceValues)
Expand Down
3 changes: 2 additions & 1 deletion nitrogen/generated/android/c++/JFunc_void_double.hpp

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions nitrogen/generated/android/c++/JHybridCactusFileSystemSpec.cpp

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions nitrogen/generated/ios/Cactus-Swift-Cxx-Bridge.cpp

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions nitrogen/generated/ios/swift/DeviceInfo.swift

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion nitrogen/generated/ios/swift/Func_void.swift

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion nitrogen/generated/ios/swift/Func_void_DeviceInfo.swift

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion nitrogen/generated/ios/swift/Func_void_bool.swift

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion nitrogen/generated/ios/swift/Func_void_double.swift

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion nitrogen/generated/ios/swift/Func_void_std__string.swift

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion nitrogen/generated/ios/swift/HybridCactusCryptoSpec.swift

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading