Repository: asciinema/asciinema (language: rust)

Issue:
Ugly stacktrace when the target directory for the cast doesn't exist
```
$ asciinema rec foo/bar/qux
asciinema: recording asciicast to foo/bar/qux
asciinema: press <ctrl-d> or type "exit" when you're done
bash-5.2$ Process Process-2:
                            Traceback (most recent call last):
                                                                File "/usr/local/Cellar/python@3.11/3.11.2_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/process.py", line 314, in _bootstrap
                                                                                                                                                                                                                              self.run()
                                                                                                                                                                                                                                          File "/usr/local/Cellar/python@3.11/3.11.2_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/process.py", line 108, in run
                                                                                                                                   self._target(*self._args, **self._kwargs)
                                                                                                                                                                              File "/usr/local/Cellar/asciinema/2.2.0/libexec/lib/python3.11/site-packages/asciinema/recorder.py", line 141, in run
                                         with self.writer as w:
                                                                 File "/usr/local/Cellar/asciinema/2.2.0/libexec/lib/python3.11/site-packages/asciinema/asciicast/v2.py", line 126, in __enter__
                                                                                                                                                                                                    self._open_file()
                                                                                                                                                                                                                       File "/usr/local/Cellar/asciinema/2.2.0/libexec/lib/python3.11/site-packages/asciinema/asciicast/v2.py", line 156, in _open_file
                                                                                             self.file = open(
                                                                                                                              ^^^^^
                                                                                                                                   FileNotFoundError: [Errno 2] No such file or directory: 'foo/bar/qux'
                                                                                                                                                                                                        Traceback (most recent call last):
  File "/usr/local/bin/asciinema", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/usr/local/Cellar/asciinema/2.2.0/libexec/lib/python3.11/site-packages/asciinema/__main__.py", line 215, in main
    code = command.execute()
           ^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/asciinema/2.2.0/libexec/lib/python3.11/site-packages/asciinema/commands/record.py", line 106, in execute
    recorder.record(
  File "/usr/local/Cellar/asciinema/2.2.0/libexec/lib/python3.11/site-packages/asciinema/recorder.py", line 61, in record
    with async_writer(sync_writer, time_offset, record_stdin) as _writer:
  File "/usr/local/Cellar/asciinema/2.2.0/libexec/lib/python3.11/site-packages/asciinema/async_worker.py", line 35, in __exit__
    raise RuntimeError(
RuntimeError: worker process exited with code 1
```
Error when attempting to run using Ubuntu 22.04 with Pip 22.0.2 / Python 3.10
I installed it and tried to run, and it came back with permission error

```
stew@stew /> sudo apt install ascicinema
[sudo] password for stew: 
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
E: Unable to locate package ascicinema
stew@stew / [100]> pip3 install asciinema
Defaulting to user installation because normal site-packages is not writeable
Collecting asciinema
  Downloading asciinema-2.2.0-py3-none-any.whl (92 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━ 92.5/92.5 KB 558.2 kB/s eta 0:00:00
Installing collected packages: asciinema
Successfully installed asciinema-2.2.0
stew@stew /> asciinema rec first.cast
asciinema: recording asciicast to first.cast
asciinema: press <ctrl-d> or type "exit" when you're done
Process Process-2:
                  Traceback (most recent call last):
                                                      File "/usr/lib/python3.10/multiprocessing/process.py", line 314, in _bootstrap
      self.run()
                  File "/usr/lib/python3.10/multiprocessing/process.py", line 108, in run
                            self._target(*self._args, **self._kwargs)
      File "/home/stew/.local/lib/python3.10/site-packages/asciinema/recorder.py", line 141, in run
                                      with self.writer as w:
                                                              File "/home/stew/.local/lib/python3.10/site-packages/asciinema/asciicast/v2.py", line 126, in __enter__
                                       self._open_file()
                                                          File "/home/stew/.local/lib/python3.10/site-packages/asciinema/asciicast/v2.py", line 156, in _open_file
                                    self.file = open(
                                                     PermissionError: [Errno 13] Permission denied: 'first.cast'
                                               Traceback (most recent call last):
  File "/home/stew/.local/bin/asciinema", line 8, in <module>
    sys.exit(main())
  File "/home/stew/.local/lib/python3.10/site-packages/asciinema/__main__.py", line 215, in main
    code = command.execute()
  File "/home/stew/.local/lib/python3.10/site-packages/asciinema/commands/record.py", line 106, in execute
    recorder.record(
  File "/home/stew/.local/lib/python3.10/site-packages/asciinema/recorder.py", line 61, in record
    with async_writer(sync_writer, time_offset, record_stdin) as _writer:
  File "/home/stew/.local/lib/python3.10/site-packages/asciinema/async_worker.py", line 35, in __exit__
    raise RuntimeError(
RuntimeError: worker process exited with code 1
```



Discussion / hints:
Yeah, those are ugly indeed :)
Attempting to use sudo did not work because it cannot find the pip app
```
stew@stew / [1]> sudo asciinema rec first.cast
sudo: asciinema: command not found
```
Output of pip
```
stew@stew / [127]> pip3 --version
pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)
stew@stew /> 
```
* Ubuntu 22.04 LTS

Fix patch:
diff --git a/asciinema/commands/record.py b/asciinema/commands/record.py
index d60cdf547..0c87704a9 100644
--- a/asciinema/commands/record.py
+++ b/asciinema/commands/record.py
@@ -53,6 +53,12 @@ def execute(self) -> int:
             upload = True
 
         if self.filename == "-":
+            if sys.stdout.isatty():
+                self.print_error(
+                    f"when recording to stdout it must not be TTY - forgot to pipe?"
+                )
+                return 1
+
             append = False
 
         elif os.path.exists(self.filename):
@@ -76,16 +82,30 @@ def execute(self) -> int:
                 )
                 return 1
 
-        elif append:
-            self.print_warning(
-                f"{self.filename} does not exist, not appending"
-            )
-            append = False
+        else:
+            dir_path = os.path.dirname(self.filename)
+
+            if not os.path.exists(dir_path):
+                self.print_error(f"directory {dir_path} doesn't exist")
+                return 1
+
+            if not os.access(dir_path, os.W_OK):
+                self.print_error(f"directory {dir_path} is not writable")
+                return 1
+
+            if append:
+                self.print_warning(
+                    f"{self.filename} does not exist, not appending"
+                )
+                append = False
 
         if append:
             self.print_info(f"appending to asciicast at {self.filename}")
         else:
-            self.print_info(f"recording asciicast to {self.filename}")
+            if self.filename == "-":
+                self.print_info(f"recording asciicast to stdout")
+            else:
+                self.print_info(f"recording asciicast to {self.filename}")
 
         if self.command:
             self.print_info("""exit opened program when you're done""")
@@ -118,6 +138,9 @@ def execute(self) -> int:
                 cols_override=self.cols_override,
                 rows_override=self.rows_override,
             )
+        except IOError as e:
+            self.print_error(f"I/O error: {str(e)}")
+            return 1
         except v2.LoadError:
             self.print_error(
                 "can only append to asciicast v2 format recordings"
@@ -155,7 +178,7 @@ def execute(self) -> int:
                     f"retry later by running: asciinema upload {self.filename}"
                 )
                 return 1
-        else:
+        elif self.filename != "-":
             self.print_info(f"asciicast saved to {self.filename}")
 
         return 0
diff --git a/asciinema/file_writer.py b/asciinema/file_writer.py
index c07e2d9a9..c06aede8f 100644
--- a/asciinema/file_writer.py
+++ b/asciinema/file_writer.py
@@ -36,8 +36,11 @@ def _write(self, data: Any) -> None:
                 self.on_error("Output pipe reopened successfully")
                 self.file.write(data)  # type: ignore
             else:
-                self.on_error("Output pipe broken")
+                self.on_error("Output pipe broken, recording suspended")
                 raise e
+        except IOError as e:
+            self.on_error("Write error, recording suspended")
+            raise e
 
 
 def noop(_: Any) -> None:
diff --git a/asciinema/recorder.py b/asciinema/recorder.py
index 504575083..dcb473761 100644
--- a/asciinema/recorder.py
+++ b/asciinema/recorder.py
@@ -132,16 +132,20 @@ def write_stdout(self, ts: float, data: Any) -> None:
         self.enqueue([ts, "o", data])
 
     def run(self) -> None:
-        with self.writer as w:
-            event: Tuple[float, str, Any]
+        try:
+            with self.writer as w:
+                event: Tuple[float, str, Any]
+                for event in iter(self.queue.get, None):
+                    assert event is not None
+                    ts, etype, data = event
+
+                    if etype == "o":
+                        w.write_stdout(self.time_offset + ts, data)
+                    elif etype == "i":
+                        w.write_stdin(self.time_offset + ts, data)
+        except IOError:
             for event in iter(self.queue.get, None):
-                assert event is not None
-                ts, etype, data = event
-
-                if etype == "o":
-                    w.write_stdout(self.time_offset + ts, data)
-                elif etype == "i":
-                    w.write_stdin(self.time_offset + ts, data)
+                pass
 
 
 class async_notifier(async_worker):


Test patch: