Skip to content

Commit 17f529e

Browse files
committed
fix: make LiveSync writes atomic instead of locking the runtime's reads
m_fileWriteMutex paired LiveSync's file writes against Runtime::ReadFileText so the runtime would not compile a half-written hot-synced module. It could not deliver that, for three independent reasons: - It is a non-static member, so LiveSync locking one runtime excludes nothing on a worker's thread - precisely the case that matters now that workers run on their own detached threads. - The lock was taken after prepareFile() deleted the file and the FileOutputStream constructor truncated it, so it covered only fos.write and not the window a reader actually hits. - The finally block called unlock() even when lock() had never been reached (a throw from mkdirs() or the FileOutputStream constructor). Unlocking a std::mutex this thread does not own is undefined behaviour, and on bionic can release a lock the JS thread holds inside ReadFileText. Writes now land through a sibling temp file renamed over the target. rename(2) within a directory is atomic, so a reader gets either the whole previous file or the whole new one, on every runtime and every thread, with no shared state to scope wrongly. That also closes the delete/truncate window, which no reader-side lock could have covered, and takes a mutex off every debug module read. java.nio.file's ATOMIC_MOVE is unavailable at minSdk 21, so this uses File.createTempFile in the target's own directory plus File.renameTo, which maps to rename(2) on every API level. The temp file must be a sibling: rename is only atomic within one filesystem. m_fileWriteMutex, Runtime::Lock/Unlock, both lock_guards, the two JNI entry points and the private native lock/unlock declarations are removed. com.tns.Runtime.lock()/unlock() remain as deprecated no-ops: they are public API and external tooling may call them. The test suite does not exercise LiveSync, so this is verified by the arm64-v8a build and by the suite not regressing (1038/1038). Hot-sync itself needs a manual check.
1 parent 7c9c3db commit 17f529e

5 files changed

Lines changed: 42 additions & 62 deletions

File tree

test-app/app/src/debug/java/com/tns/NativeScriptSyncServiceSocketImpl.java

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -357,23 +357,45 @@ private int getLength() {
357357
return lengthInt;
358358
}
359359

360+
/*
361+
* Written through a sibling temp file renamed over the target, because
362+
* the app keeps running JS while files stream in: rename(2) within a
363+
* directory is atomic, so a runtime thread that requires this path
364+
* mid-sync gets either the whole previous file or the whole new one.
365+
* Writing in place cannot be made safe from the reader side -- the
366+
* truncate lands before any lock a reader could share, and each runtime
367+
* (main, every worker) reads on its own thread.
368+
*/
360369
private void createOrOverrideFile(String fileName, byte[] content) throws IOException {
361-
File fileToCreate = prepareFile(fileName);
362-
try {
370+
File fileToCreate = new File(DEVICE_APP_DIR, fileName);
371+
File parentDir = fileToCreate.getParentFile();
363372

364-
fileToCreate.getParentFile().mkdirs();
365-
FileOutputStream fos = new FileOutputStream(fileToCreate.getCanonicalPath());
366-
if(runtime != null) {
367-
runtime.lock();
373+
if (parentDir != null) {
374+
parentDir.mkdirs();
375+
}
376+
377+
File temp = null;
378+
try {
379+
// Same directory as the target: rename is only atomic within one
380+
// filesystem, and a sibling is the one placement that guarantees it.
381+
temp = File.createTempFile("livesync", ".tmp", parentDir);
382+
383+
FileOutputStream fos = new FileOutputStream(temp);
384+
try {
385+
fos.write(content);
386+
} finally {
387+
fos.close();
368388
}
369-
fos.write(content);
370-
fos.close();
371389

390+
if (!temp.renameTo(fileToCreate)) {
391+
throw new IOException(String.format("failed to rename %s onto the target", temp.getAbsolutePath()));
392+
}
393+
temp = null;
372394
} catch (Exception e) {
373395
throw new IOException(String.format("\nLiveSync: failed to write file: %s\nOriginal Exception: %s", fileName, e.toString()));
374396
} finally {
375-
if(runtime != null) {
376-
runtime.unlock();
397+
if (temp != null) {
398+
temp.delete();
377399
}
378400
}
379401
}
@@ -387,14 +409,6 @@ void deleteRecursive(File fileOrDirectory) {
387409
fileOrDirectory.delete();
388410
}
389411

390-
private File prepareFile(String fileName) {
391-
File fileToCreate = new File(DEVICE_APP_DIR, fileName);
392-
if (fileToCreate.exists()) {
393-
fileToCreate.delete();
394-
}
395-
return fileToCreate;
396-
}
397-
398412
/*
399413
* Reads next bites from input stream. Bytes read depend on passed parameter.
400414
* */

test-app/runtime/src/main/cpp/Runtime.cpp

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -335,31 +335,13 @@ Runtime::~Runtime() {
335335
}
336336

337337
std::string Runtime::ReadFileText(const std::string& filePath) {
338-
#ifdef APPLICATION_IN_DEBUG
339-
std::lock_guard<std::mutex> lock(m_fileWriteMutex);
340-
#endif
341338
return File::ReadText(filePath);
342339
}
343340

344341
std::string Runtime::ReadFileText(const std::string& filePath, bool& ok) {
345-
#ifdef APPLICATION_IN_DEBUG
346-
std::lock_guard<std::mutex> lock(m_fileWriteMutex);
347-
#endif
348342
return File::ReadText(filePath, ok);
349343
}
350344

351-
void Runtime::Lock() {
352-
#ifdef APPLICATION_IN_DEBUG
353-
m_fileWriteMutex.lock();
354-
#endif
355-
}
356-
357-
void Runtime::Unlock() {
358-
#ifdef APPLICATION_IN_DEBUG
359-
m_fileWriteMutex.unlock();
360-
#endif
361-
}
362-
363345
// The boot backstop: hold the launching thread until boot has actually
364346
// finished. Two independent things can leave it unfinished, and BOTH must hold
365347
// the pump — an in-flight module-graph load, and an entry whose own evaluation

test-app/runtime/src/main/cpp/Runtime.h

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,6 @@ class Runtime {
145145
jboolean PassExceptionToJsNative(JNIEnv* env, jobject obj, jthrowable exception, jstring message, jstring fullStackTrace, jstring jsStackTrace, jboolean isDiscarded);
146146
void DestroyRuntime();
147147

148-
void Lock();
149-
void Unlock();
150-
151148
int GetId();
152149

153150
v8::Local<v8::Context> GetContext();
@@ -392,9 +389,6 @@ class Runtime {
392389

393390
static thread_local Runtime* s_currentRuntime;
394391

395-
#ifdef APPLICATION_IN_DEBUG
396-
std::mutex m_fileWriteMutex;
397-
#endif
398392
};
399393
}
400394

test-app/runtime/src/main/cpp/com_tns_Runtime.cpp

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -285,20 +285,6 @@ extern "C" JNIEXPORT jboolean Java_com_tns_Runtime_notifyGcLegacy(JNIEnv* env, j
285285
return notifyGcFast_impl(env, obj, runtimeId);
286286
}
287287

288-
extern "C" JNIEXPORT void Java_com_tns_Runtime_lock(JNIEnv* env, jobject obj, jint runtimeId) {
289-
auto runtime = TryGetRuntime(runtimeId);
290-
if (runtime != nullptr) {
291-
runtime->Lock();
292-
}
293-
}
294-
295-
extern "C" JNIEXPORT void Java_com_tns_Runtime_unlock(JNIEnv* env, jobject obj, jint runtimeId) {
296-
auto runtime = TryGetRuntime(runtimeId);
297-
if (runtime != nullptr) {
298-
runtime->Unlock();
299-
}
300-
}
301-
302288
extern "C" JNIEXPORT jboolean Java_com_tns_Runtime_passExceptionToJsNative(JNIEnv* env, jobject obj, jint runtimeId, jthrowable exception, jstring message, jstring fullStackTrace, jstring jsStackTrace, jboolean isDiscarded) {
303289
auto runtime = TryGetRuntime(runtimeId);
304290
if (runtime == nullptr) {

test-app/runtime/src/main/java/com/tns/Runtime.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,6 @@ private boolean notifyGc(int runtimeId) {
7171
return SUPPORTS_OPTIMIZED_NATIVE ? notifyGcFast(runtimeId) : notifyGcLegacy(runtimeId);
7272
}
7373

74-
private native void lock(int runtimeId);
75-
76-
private native void unlock(int runtimeId);
77-
7874
private native boolean passExceptionToJsNative(int runtimeId, Throwable ex, String message, String fullStackTrace, String jsStackTrace, boolean isDiscarded);
7975

8076
@CriticalNative
@@ -766,12 +762,20 @@ public void notifyGc() {
766762
notifyGc(runtimeId);
767763
}
768764

765+
/**
766+
* @deprecated No-op. This paired the runtime's file reads against LiveSync's
767+
* writes, which now land atomically via a temp file renamed over the target,
768+
* so readers need no lock. Retained because it is public API.
769+
*/
770+
@Deprecated
769771
public void lock() {
770-
lock(runtimeId);
771772
}
772773

774+
/**
775+
* @deprecated No-op. See {@link #lock()}.
776+
*/
777+
@Deprecated
773778
public void unlock() {
774-
unlock(runtimeId);
775779
}
776780

777781
public static void initInstanceFromPossibleNonMainThread(final Object instance) {

0 commit comments

Comments
 (0)