Repository: iterative/dvc

Issue:
Interrupting DVC during checkout produces error in .dvc/link.state
Hi guys, I noticed that interrupting DVC during checkout can produce incomplete `.dvc/link.state` - the last "}" is missing. This results in the following error message when running any DVC command:

```
Initialization error: Expecting ',' delimiter: line 1 column 136782 (char 136781)
```

Is there a way to save the file properly before exiting?

I'm using version 0.9.7.

Stack trace:
```
  File ".../anaconda3/bin/dvc", line 11, in <module>
    sys.exit(main())
  File ".../anaconda3/lib/python3.6/site-packages/dvc/main.py", line 22, in main
    ret = cmd.run_cmd()
  File ".../anaconda3/lib/python3.6/site-packages/dvc/command/common/base.py", line 35, in run_cmd
    return self.run()
  File ".../anaconda3/lib/python3.6/site-packages/dvc/command/data_sync.py", line 18, in run
    if self.do_run(target):
  File ".../anaconda3/lib/python3.6/site-packages/dvc/command/data_sync.py", line 26, in do_run
    self.project.pull(target=target, jobs=self.args.jobs, remote=self.args.remote)
  File ".../anaconda3/lib/python3.6/site-packages/dvc/project.py", line 231, in pull
    self.checkout()
  File ".../anaconda3/lib/python3.6/site-packages/dvc/project.py", line 194, in checkout
    stage.checkout()
  File ".../anaconda3/lib/python3.6/site-packages/dvc/stage.py", line 246, in checkout
    out.checkout()
  File ".../anaconda3/lib/python3.6/site-packages/dvc/output.py", line 198, in checkout
    self.project.cache.checkout(self.path, self.md5)
  File ".../anaconda3/lib/python3.6/site-packages/dvc/cache.py", line 177, in checkout
    return self._checkout(path, md5)
  File ".../anaconda3/lib/python3.6/site-packages/dvc/cache.py", line 173, in _checkout
    self.link(c, p)
  File ".../anaconda3/lib/python3.6/site-packages/dvc/cache.py", line 95, in link
    self.link_state.update(link)
  File ".../anaconda3/lib/python3.6/site-packages/dvc/state.py", line 195, in update
    self.dump()
  File ".../anaconda3/lib/python3.6/site-packages/dvc/state.py", line 99, in dump
    json.dump(self._db, fd)
  File ".../anaconda3/lib/python3.6/json/__init__.py", line 179, in dump
    for chunk in iterable:
  File ".../anaconda3/lib/python3.6/json/encoder.py", line 430, in _iterencode
    yield from _iterencode_dict(o, _current_indent_level)
  File ".../anaconda3/lib/python3.6/json/encoder.py", line 404, in _iterencode_dict
    yield from chunks
KeyboardInterrupt

```


Discussion:
It has been interrupted right when it was writing the file. We should protect such pieces of code with a signal handler or something and if corruption still occurs, dvc should probably remove that file and start over. I'll look into it shortly, thank you!

For now, as a workaround, you should be safe deleting or fixing that link.state file by-hand.

Resolving patch:
diff --git a/dvc/signal_handler.py b/dvc/signal_handler.py
new file mode 100644
--- /dev/null
+++ b/dvc/signal_handler.py
@@ -0,0 +1,17 @@
+import signal
+import threading
+
+from dvc.logger import Logger
+
+
+class SignalHandler(object):
+    def __enter__(self):
+        if isinstance(threading.current_thread(), threading._MainThread):
+            self.old_handler = signal.signal(signal.SIGINT, self.handler)
+
+    def handler(self, sig, frame):
+        Logger.debug('Ignoring SIGINT during critical parts of code...')
+
+    def __exit__(self, type, value, traceback):
+        if isinstance(threading.current_thread(), threading._MainThread):
+            signal.signal(signal.SIGINT, self.old_handler)
diff --git a/dvc/state.py b/dvc/state.py
--- a/dvc/state.py
+++ b/dvc/state.py
@@ -7,6 +7,8 @@
 from dvc.system import System
 from dvc.utils import file_md5, dict_md5, remove
 from dvc.exceptions import DvcException
+from dvc.signal_handler import SignalHandler
+from dvc.logger import Logger
 
 
 class StateEntry(object):
@@ -96,14 +98,19 @@ def load(self):
             return {}
 
         with open(self.state_file, 'r') as fd:
-            return json.load(fd)
+            try:
+                return json.load(fd)
+            except ValueError as exc:
+                Logger.error('Failed to load \'{}\''.format(self.state_file), exc)
+                return {}
 
     def dump(self):
         if not self.state_file:
             return
 
-        with open(self.state_file, 'w+') as fd:
-            json.dump(self._db, fd)
+        with SignalHandler():
+            with open(self.state_file, 'w+') as fd:
+                json.dump(self._db, fd)
 
     @staticmethod
     def mtime(path):


Test patch: