Skip to content

Commit

Permalink
1.1.1: Detect open autosave files (is_active)
Browse files Browse the repository at this point in the history
Enables testing whether an autosave file is corresponding to a running process.
  • Loading branch information
2xB committed Nov 29, 2022
1 parent 9c75a66 commit 16ff9ac
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 9 deletions.
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,21 @@ Usually, after a crash through e.g. a segmentation fault or a power outage, data

2. `faultguard` can save the selected data automatically in customizable time intervals to a file from which it can be recovered on the next application launch.

### Example

An example using all features of `faultguard` can be found in `example.py`.

To secure an application data using `faultguard`, you define a `launch` function that `faultguard` provides with a custom data dictionary. This dictionary, although working like a usual dictionary and accepting all content that can be serialized using `pickle`, is automatically backed up as described above. If the guarded application crashes, the backup process launches a crash handler in form of a `rescue` function also defined by you and provides it with the backed up dictionary. Additionally, if you provide `faultguard` with a time interval and a path for autosaves, it stores the data on disk and you can call the `recover` method to recover the file content and call your `rescue` function.
### Usage

To secure an application data using `faultguard`, you define a `launch` function that receives a custom data dictionary from `faultguard`. This dictionary, although working like a usual dictionary and accepting all content that can be serialized using `pickle`, is automatically backed up as described above. If the guarded application crashes, the backup process launches a crash handler in form of a `rescue` function also defined by you and provides it with the backed up dictionary. Additionally, if you provide `faultguard` with a time interval and a path for autosaves, it stores the data on disk and you can call the `recover` method to recover the file content and call your `rescue` function. `faultguard` will raise a `RuntimeError` when trying to write to an existing autosave file or reading the autosave file of a running process.

The `faultguard` interface is very simple - you just provide it with a `launch` and a `rescue` function and everything else works automatically. If you use autosaving, on application launch you should additionally check for backup files and use `is_active` to see if the process corresponding to an autosave file is still active. If not, that would show that `faultguard` did previously not exit properly, so you can then let `faultguard` `recover` the file.

The `faultguard` interface is very simple - you just provide it with a `launch` and a `rescue` function and everything else works automatically. If you use autosaving, on application launch you should additionally test if a backup file exists, which would show that `faultguard` did previously not exit properly. If a backup file exists, you should let `faultguard` recover it and then delete it to make place for a new one.
### Technical description

On the technical side, the in-memory backup is realized through Python modules `pickle`, `multiprocessing` and `collections`, which are used to serialize and deserialize various types of data and provide the dictionary-like data type that is available in both the guarded application and the rescue handler process. The autosave functionality uses the Python module `lzma` for efficient compression of autosave files and `os` for file handling.
On the technical side, the in-memory backup is realized through Python modules `pickle`, `multiprocessing` and `collections`, which are used to serialize and deserialize various types of data and provide the dictionary-like data type that is available in both the guarded application and the rescue handler process.
The Python module `signal` is used to ensure signals like keyboard interrupts are handled correctly and received by the guarded process.
The autosave functionality uses the Python module `lzma` for efficient compression of autosave files, `os` for file handling and `time` for measuring the time since a process corresponding to a backup file was last active.

Feel encouraged to look into the source code and to contribute through (well documented :D ) pull requests!

Expand Down
8 changes: 7 additions & 1 deletion example.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import numpy as np
import os
import time
import sys

def launch(faultguard_data, args):
"""
Expand Down Expand Up @@ -61,7 +62,12 @@ def recover(faultguard_data):
def main(use_autosave=True):
if use_autosave:
if os.path.isfile("test.tmp.xz"):
print("Autosave exists:")
print("Autosave exists.")

if faultguard.is_active("test.tmp.xz"):
print("Trying to launch the example twice. Since it uses a static autosave file path, starting it twice with autosave enabled does not work.")
sys.exit(-1)

faultguard.recover(recover, "test.tmp.xz")
os.remove("test.tmp.xz")

Expand Down
32 changes: 28 additions & 4 deletions faultguard.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,18 @@ def wrapped_launch(launch, managed_dict, signal_handlers, args):
else:
launch(faultguard_data, args)

def recover(rescue, autosave_file=None):
def is_active(autosave_file):
"""
Test if the process creating a given autosave file is running.
:param autosave_file: Path to autosave file.
"""
import os
import time

return abs(os.stat(autosave_file).st_atime - time.time()) < 2

def recover(rescue, autosave_file):
"""
Load the given faultguard data dictionary from an autosave file and pass it to a rescue function.
Expand All @@ -50,6 +61,12 @@ def recover(rescue, autosave_file=None):
# Compression library
import lzma
import os

if is_active(autosave_file):
import time
time.sleep(2)
if is_active(autosave_file):
raise RuntimeError("Trying to access a backup of a process that is still running.")

success = True
try:
Expand Down Expand Up @@ -133,18 +150,25 @@ def start(launch, rescue, args=None, autosave_interval=None, autosave_file=None)
else:
# Compression library
import lzma
import time

while p.is_alive():
# Wait for next autosave
p.join(autosave_interval)

# Autosave
if os.path.isfile(autosave_file + ".tmp"):
os.remove(autosave_file + ".tmp")
os.rename(autosave_file, autosave_file + ".tmp")

with lzma.open(autosave_file, "w") as f:
pickle.dump(dict(managed_dict), f)
mod_time = time.time()

# Wait for next autosave
remaining_interval = autosave_interval
while remaining_interval > 1:
remaining_interval -= 1
p.join(1)
os.utime(autosave_file, (time.time(), mod_time))
p.join(remaining_interval)

# Close Manager process
# If this is not done and the faultguard process is terminated, the Manager process
Expand Down
11 changes: 10 additions & 1 deletion test_autosave.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,20 @@ def test_main():
# Prepare test environment
if os.path.isfile("test.tmp.xz"):
os.remove("test.tmp.xz")
if os.path.isfile("test.tmp.xz.tmp"):
os.remove("test.tmp.xz.tmp")

p = Process(target=run_test)

# Run process
p.start()
p.join(2)
p.join(3)
assert faultguard.is_active("test.tmp.xz")
os.rename("test.tmp.xz", "test.tmp.xz.backup")
p.join()
import time
time.sleep(3)
assert not faultguard.is_active("test.tmp.xz.backup")

os.rename("test.tmp.xz.backup", "test.tmp.xz")
assert faultguard.recover(recover, "test.tmp.xz") == 0
Expand All @@ -69,3 +75,6 @@ def test_main():
with open("test.tmp.xz", "w") as f:
f.write("Test")
assert faultguard.recover(recover, "test.tmp.xz") == 1

os.remove("test.tmp.xz")
os.remove("test.tmp.xz.tmp")

0 comments on commit 16ff9ac

Please sign in to comment.