Skip to content

Conversation

dhirschfeld
Copy link
Contributor

@dhirschfeld dhirschfeld commented Jun 13, 2017

On Windows os.stat treats host mapped volumes as broken symlinks

xref: jupyterlab/jupyterlab#2426

@dhirschfeld dhirschfeld changed the title Workaround for Windows Containers [WIP] Workaround for Windows Containers Jun 13, 2017

if not stat.S_ISREG(st.st_mode) and not stat.S_ISDIR(st.st_mode):
self.log.debug("%s not a regular file", os_path)
self.log.warning("%s not a regular file", os_path)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just made this change to confirm why the mapped volume still isn't showing.
Results in warnings being printed to the terminal:

[W 17:07:19.450 LabApp] C:\jupyter\notebooks not a regular file
[W 17:07:29.451 LabApp] C:\jupyter\notebooks not a regular file
[W 17:07:40.452 LabApp] C:\jupyter\notebooks not a regular file

self.log.warning("%s not a regular file", os_path)
continue

if self.should_list(name) and not is_file_hidden(os_path, stat_res=st):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

is_file_hidden(os_path, stat_res=st) returns True for a host mapped volume.

@dhirschfeld
Copy link
Contributor Author

Working around the fact that the mapped directory is hidden results in

[I 17:32:32.350 LabApp] Refusing to serve hidden directory 'C:\\jupyter\\notebooks', via 404 Error
[W 17:32:32.350 LabApp] directory does not exist: 'notebooks'
[W 17:32:32.350 LabApp] 404 GET /api/contents/?content=1&1497339152344 (172.29.208.1) 4.00ms referer=http://b7c8e663c8c1:8888/lab?

@dhirschfeld
Copy link
Contributor Author

🐇 🕳️

@dhirschfeld
Copy link
Contributor Author

For reference:
image

@dhirschfeld
Copy link
Contributor Author

dhirschfeld commented Jun 13, 2017

...however:

[19]: from notebook.utils import is_file_hidden_win

[20]: st = os.stat(r"C:\jupyter\notebooks", follow_symlinks=False)
... : st
Out[20]: os.stat_result(st_mode=41471, st_ino=281474976710694, st_dev=113970060, st_nlink=1, st_uid=0, st_gid=0, st_size=0, st_atime=1497345137, st_mtime=1497345137, st_ctime=1497345137)

[21]: st.st_file_attributes
Out[21]: 1040

[22]: bool(st.st_file_attributes & stat.FILE_ATTRIBUTE_HIDDEN)
Out[22]: False

[23]: is_file_hidden_win(r"C:\jupyter\notebooks")
Out[23]: True

From: https://stackoverflow.com/a/6365265/2960140

The is_file_hidden_win function does essentially the same thing, but with an extra check that it can be stat'd. I'm not sure what that's there for but that won't work on a host mapped volume in a Windows Container.

@takluyver - can you shed any light on the purpose of those lines? Can they just be removed?

# check that dirs can be listed

Edit: Removing the above lines does allow JupyterLab to list and use the host mapped volume
image

@dhirschfeld dhirschfeld force-pushed the windows-containers branch 2 times, most recently from 6e078d5 to bf1e410 Compare June 14, 2017 00:15
@dhirschfeld
Copy link
Contributor Author

Cleaned up the implementation and (hopefully) made it py27 compatible

@dhirschfeld
Copy link
Contributor Author

dhirschfeld commented Jun 14, 2017

RFR: @takluyver, @minrk

@takluyver - I've included you in the request for review as you appear to be the author of the is_file_hidden_win function.

The use of os.lstat which doesn’t follow symlinks may render that code block redundant, but it will only occur inside a Windows Container where that code block wouldn’t work at all (on mapped directories) so it’s likely an improvement over the current situation!

@dhirschfeld dhirschfeld changed the title [WIP] Workaround for Windows Containers Workaround for Windows Containers Jun 14, 2017
@dhirschfeld
Copy link
Contributor Author

I'm not sure why travis has failed - I can't see anything that should be the case since the changes should only apply on Windows (and when inside a container). Random failure?

# Replace `os.stat` which can't stat a host mapped volume from inside a Windows Container.
WINDOWS_CONTAINER = sys.platform == 'win32' and os.environ.get('USERNAME') == 'ContainerAdministrator'
if WINDOWS_CONTAINER:
f_stat = os.lstat
Copy link
Member

Choose a reason for hiding this comment

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

should we just use lstat on windows in general, rather than trying to check if it's a container?

Copy link
Member

Choose a reason for hiding this comment

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

Can we do this for Windows in general, and remove the WINDOWS_CONTAINER constant?

@minrk
Copy link
Member

minrk commented Jun 14, 2017

I restarted Travis. Intermittent failure unrelated to the PR.

self.log.warning("%s doesn't exist", os_path)
else:
self.log.warning("Error stat-ing %s: %s", os_path, e)
self.log.warning("Error stat-ing %s: %s", (os_path, e))
Copy link
Member

Choose a reason for hiding this comment

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

Adding these parentheses will cause a formatting error:

TypeError: not enough arguments for format string

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, not sure how that change crept in - I'll blame it on auto-formatting!

@minrk
Copy link
Member

minrk commented Jun 14, 2017

Is there a way we can avoid detecting and special-casing Windows containers, and do something that works on Windows and/or catch errors appropriately, instead?

@dhirschfeld
Copy link
Contributor Author

I agree it's a pretty ugly hack - I was aiming to just fix my usecase without changing the existing behaviour for anyone else.

There's probably a better fix, but I'm not 100% sure why all the existing checks (some of which fail on Windows containers) are there so I just tried a minimal fix.

Specifically, the is_file_hidden_win provides a py2/3 compatible check for the hidden attribute:

ishidden = bool(st.st_file_attributes & stat.FILE_ATTRIBUTE_HIDDEN)  # py3 only

...but it also tries to call os.stat on the path with the comment that it's checking if the directory can be listed:

    # check that dirs can be listed
    if os.path.isdir(abs_path):
        # can't trust os.access on Windows because it seems to always return True
        try:
            os.stat(abs_path)
        except OSError:
            # stat may fail on Windows junctions or non-user-readable dirs
            return True

I assume by calling os.stat it's just checking if the path is a broken symlink which then implies it's not listable... unless it's a host mapped directory from within a Windows Container.

My initial reaction was to simply delete that block entirely as I'm not sure why its necessary. I think it would be better to show a "broken" symlink dir than to pretend it never existed. When trying to change to a broken directory the notebook could then just let the underlying OSError bubble up to a dialog for the user. In the case of a host mapped directory there would be no underlying OSError and changing to the directory would Just Work.

@dhirschfeld
Copy link
Contributor Author

IIUC by just using lstat on Windows you would be forgoing the checks for broken symlinks. I'm not too sure how important that is and as above I think it may even be better to show broken symlinks rather than hide them.

@dhirschfeld
Copy link
Contributor Author

Other than is_file_hidden_win there are a fewother checks in the FileContentsManager which are problematic in a Windows Container:

The os.exists check will fail on a host mapped dir. This was resolved by using lstat

The stat check in _dir_model was similarly converted to an lstat

And finally, the check for a regular file:

if not stat.S_ISREG(st.st_mode) and not stat.S_ISDIR(st.st_mode):
    self.log.debug("%s not a regular file", os_path)
    continue

This was resolved by protecting with a if not WINDOWS_CONTAINER clause.

Again though I'm not sure why there's a requirement to protect against listing non-regular (??) files. I think for a file browser the requirement could just be if it shows up with ls/dir then display it in the file browser?

If that's acceptable you could then just delete the whole checking block in _dir_model:

try:
    st = os.stat(os_path)
except OSError as e:
    # skip over broken symlinks in listing
    if e.errno == errno.ENOENT:
        self.log.warning("%s doesn't exist", os_path)
    else:
        self.log.warning("Error stat-ing %s: %s", os_path, e)
    continue

if not stat.S_ISREG(st.st_mode) and not stat.S_ISDIR(st.st_mode):
    self.log.debug("%s not a regular file", os_path)
    continue

@dhirschfeld
Copy link
Contributor Author

So, my recommendation would actually be to remove all checking from both functions and list all files whether regular or not or broken symlinks or not. If the user then tries to use a "broken" file or directory they should get the same OS error as if they tried to do the same from the terminal.

Not sure if that makes sense or is reasonable. It's certainly a change in behaviour and thus a design question for the core devs.

@dhirschfeld
Copy link
Contributor Author

...maybe expand the contents definition to include attributes such as hidden/symlink/broken and have a UI element which would allow the user to choose whether or not to display files based on those attributes.

@minrk
Copy link
Member

minrk commented Jun 15, 2017

I'd rather not include hidden files in the listing, since hidden files should be...hidden. Broken links are different, though. I think we hid broken links because they caused errors elsewhere when stat is called, but if switching to lstat (probably across the board on Windows) causes the stat to not fail, then using that instead makes sense to me, and broken links can be included in the listing. We should just make sure that opening the URL for a broken link properly 404s instead of raising an uncaught error.

@dhirschfeld
Copy link
Contributor Author

I'll look into what happens with broken links on my branch and report back...

@dhirschfeld
Copy link
Contributor Author

On my branch, when run from JupyterHub I get an internal server error dialog when I double-click a directory symbolic link. Will investigate without JupyterHub to try and get more info from the terminal.

In contrast an NTFS Junction Point works fine in the file browser.

@dhirschfeld
Copy link
Contributor Author

Trying to start a kernel in a host mapped dir seems to cause an issue:

Error Starting Kernel
Traceback (most recent call last):
  File "C:\Miniconda3\lib\site-packages\traitlets\traitlets.py", line 528, in get
    value = obj._trait_values[self.name]
KeyError: 'root_dir'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Miniconda3\lib\site-packages\notebook\base\handlers.py", line 516, in wrapper
    result = yield gen.maybe_future(method(self, *args, **kwargs))
  File "C:\Miniconda3\lib\site-packages\tornado\gen.py", line 1055, in run
    value = future.result()
  File "C:\Miniconda3\lib\site-packages\tornado\concurrent.py", line 238, in result
    raise_exc_info(self._exc_info)
  File "<string>", line 4, in raise_exc_info
  File "C:\Miniconda3\lib\site-packages\tornado\gen.py", line 1063, in run
    yielded = self.gen.throw(*exc_info)
  File "C:\Miniconda3\lib\site-packages\notebook\services\sessions\handlers.py", line 75, in post
    type=mtype))
  File "C:\Miniconda3\lib\site-packages\tornado\gen.py", line 1055, in run
    value = future.result()
  File "C:\Miniconda3\lib\site-packages\tornado\concurrent.py", line 238, in result
    raise_exc_info(self._exc_info)
  File "<string>", line 4, in raise_exc_info
  File "C:\Miniconda3\lib\site-packages\tornado\gen.py", line 1063, in run
    yielded = self.gen.throw(*exc_info)
  File "C:\Miniconda3\lib\site-packages\notebook\services\sessions\sessionmanager.py", line 79, in create_session
    kernel_id = yield self.start_kernel_for_session(session_id, path, name, type, kernel_name)
  File "C:\Miniconda3\lib\site-packages\tornado\gen.py", line 1055, in run
    value = future.result()
  File "C:\Miniconda3\lib\site-packages\tornado\concurrent.py", line 238, in result
    raise_exc_info(self._exc_info)
  File "<string>", line 4, in raise_exc_info
  File "C:\Miniconda3\lib\site-packages\tornado\gen.py", line 1063, in run
    yielded = self.gen.throw(*exc_info)
  File "C:\Miniconda3\lib\site-packages\notebook\services\sessions\sessionmanager.py", line 92, in start_kernel_for_session
    self.kernel_manager.start_kernel(path=kernel_path, kernel_name=kernel_name)
  File "C:\Miniconda3\lib\site-packages\tornado\gen.py", line 1055, in run
    value = future.result()
  File "C:\Miniconda3\lib\site-packages\tornado\concurrent.py", line 238, in result
    raise_exc_info(self._exc_info)
  File "<string>", line 4, in raise_exc_info
  File "C:\Miniconda3\lib\site-packages\tornado\gen.py", line 307, in wrapper
    yielded = next(result)
  File "C:\Miniconda3\lib\site-packages\notebook\services\kernels\kernelmanager.py", line 92, in start_kernel
    kwargs['cwd'] = self.cwd_for_path(path)
  File "C:\Miniconda3\lib\site-packages\notebook\services\kernels\kernelmanager.py", line 66, in cwd_for_path
    os_path = to_os_path(path, self.root_dir)
  File "C:\Miniconda3\lib\site-packages\traitlets\traitlets.py", line 556, in __get__
    return self.get(obj, cls)
  File "C:\Miniconda3\lib\site-packages\traitlets\traitlets.py", line 535, in get
    value = self._validate(obj, dynamic_default())
  File "C:\Miniconda3\lib\site-packages\traitlets\traitlets.py", line 593, in _validate
    value = self._cross_validate(obj, value)
  File "C:\Miniconda3\lib\site-packages\traitlets\traitlets.py", line 599, in _cross_validate
    value = obj._trait_validators[self.name](obj, proposal)
  File "C:\Miniconda3\lib\site-packages\traitlets\traitlets.py", line 907, in __call__
    return self.func(*args, **kwargs)
  File "C:\Miniconda3\lib\site-packages\notebook\services\kernels\kernelmanager.py", line 52, in _update_root_dir
    raise TraitError("kernel root dir %r is not a directory" % value)
traitlets.traitlets.TraitError: kernel root dir 'C:\\jupyter\\dhirschf' is not a directory

@dhirschfeld
Copy link
Contributor Author

When double-clicking on a symlink directory JupyterLab pops up an Internal Server Error dialog and I get the below error in the terminal:

[E 15:45:06.909 LabApp] Unhandled error in API request
    Traceback (most recent call last):
      File "C:\Miniconda3\lib\site-packages\notebook\base\handlers.py", line 516, in wrapper
        result = yield gen.maybe_future(method(self, *args, **kwargs))
      File "C:\Miniconda3\lib\site-packages\tornado\gen.py", line 1055, in run
        value = future.result()
      File "C:\Miniconda3\lib\site-packages\tornado\concurrent.py", line 238, in result
        raise_exc_info(self._exc_info)
      File "<string>", line 4, in raise_exc_info
      File "C:\Miniconda3\lib\site-packages\tornado\gen.py", line 307, in wrapper
        yielded = next(result)
      File "C:\Miniconda3\lib\site-packages\notebook\services\contents\handlers.py", line 124, in get
        path=path, type=type, format=format, content=content,
      File "C:\Miniconda3\lib\site-packages\notebook\services\contents\filemanager.py", line 390, in get
        model = self._dir_model(path, content=content)
      File "C:\Miniconda3\lib\site-packages\notebook\services\contents\filemanager.py", line 275, in _dir_model
        for name in os.listdir(os_dir):
    FileNotFoundError: [WinError 3] The system cannot find the path specified: 'C:\\jupyter\\dhirschf\\lnk2tmp'
[E 15:45:07.048 LabApp] {
      "Host": "101026def9c6:8888",
      "Connection": "keep-alive",
      "X-Xsrftoken": "2|04f222c8|c265413a0b7b6a0a666875450653facd|1497591882",
      "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
      "Accept": "*/*",
      "Referer": "http://101026def9c6:8888/lab?",
      "Accept-Encoding": "gzip, deflate, sdch",
      "Accept-Language": "en-US,en;q=0.8",
      "Cookie": "_xsrf=2|04f222c8|c265413a0b7b6a0a666875450653facd|1497591882"
    }
[E 15:45:07.048 LabApp] 500 GET /api/contents/lnk2tmp?content=1&1497591906904 (172.29.208.1) 141.06ms referer=http://101026def9c6:8888/lab?

Note: the directory tmp does exist so the symlink isn't broken
image

@dhirschfeld
Copy link
Contributor Author

dhirschfeld commented Jun 16, 2017

...well, that's just odd - os.listdir and cd in the container both fail but when run on the host they work. I set the permissions so everyone has access / full control over both tmp and lnk2tmp folders but that made no difference so it seems like symlink directories aren't supported.

On host it Just Works:

C:\Jupyter\dhirschf>ls
Demo.ipynb  References  Tutorials  lnk2tmp  tmp

C:\Jupyter\dhirschf>python
Python 3.6.1 | packaged by conda-forge | (default, Mar 24 2017, 01:02:36) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.listdir('tmp')
[]
>>> os.listdir('lnk2tmp')
[]
>>>

@dhirschfeld
Copy link
Contributor Author

If I create instead a junction point it works fine. If I then purposely break the junction point by deleting tmp which it's pointing to I get the same Internal Server Error and traceback in the terminal

[E 16:17:59.923 LabApp] Unhandled error in API request
    Traceback (most recent call last):
      File "C:\Miniconda3\lib\site-packages\notebook\base\handlers.py", line 516, in wrapper
        result = yield gen.maybe_future(method(self, *args, **kwargs))
      File "C:\Miniconda3\lib\site-packages\tornado\gen.py", line 1055, in run
        value = future.result()
      File "C:\Miniconda3\lib\site-packages\tornado\concurrent.py", line 238, in result
        raise_exc_info(self._exc_info)
      File "<string>", line 4, in raise_exc_info
      File "C:\Miniconda3\lib\site-packages\tornado\gen.py", line 307, in wrapper
        yielded = next(result)
      File "C:\Miniconda3\lib\site-packages\notebook\services\contents\handlers.py", line 124, in get
        path=path, type=type, format=format, content=content,
      File "C:\Miniconda3\lib\site-packages\notebook\services\contents\filemanager.py", line 390, in get
        model = self._dir_model(path, content=content)
      File "C:\Miniconda3\lib\site-packages\notebook\services\contents\filemanager.py", line 275, in _dir_model
        for name in os.listdir(os_dir):
    FileNotFoundError: [WinError 3] The system cannot find the path specified: 'C:\\jupyter\\dhirschf\\lnk2tmp'
[E 16:17:59.957 LabApp] {
      "Host": "22d7b7bbfcd9:8888",
      "Connection": "keep-alive",
      "X-Xsrftoken": "2|0703062b|ad483203768cff5faa31b2a9ba65f6d1|1497593375",
      "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
      "Accept": "*/*",
      "Referer": "http://22d7b7bbfcd9:8888/lab?",
      "Accept-Encoding": "gzip, deflate, sdch",
      "Accept-Language": "en-US,en;q=0.8",
      "Cookie": "_xsrf=2|0703062b|ad483203768cff5faa31b2a9ba65f6d1|1497593375"
    }
[E 16:17:59.957 LabApp] 500 GET /api/contents/lnk2tmp?content=1&1497593879920 (172.29.208.1) 35.01ms referer=http://22d7b7bbfcd9:8888/lab?

@dhirschfeld
Copy link
Contributor Author

My suggestion would be for symlinks (which don't work) and for broken junction points to display the error text:

FileNotFoundError: [WinError 3] The system cannot find the path specified: 'C:\\jupyter\\dhirschf\\lnk2tmp'

...rather than just the opaque Internal Server Error.

But I suppose that's an issue for JupyterLab rather than the notebook.

@dhirschfeld
Copy link
Contributor Author

File soft-links also fail, but hard links seem to work:
image

[W 16:31:36.620 LabApp] Cannot read non-file C:\jupyter\dhirschf\hw-softlink.txt
[W 16:31:36.620 LabApp] 400 GET /api/contents/hw-softlink.txt?format=text&type=file&content=1&1497594696615 (172.29.208.1) 1.96ms referer=http://22d7b7bbfcd9:8888/lab?

@dhirschfeld
Copy link
Contributor Author

The symlink situation on Windows seems to be a known issue:
https://github.com/docker/docker.github.io/pull/34/files

@dhirschfeld
Copy link
Contributor Author

I'd rather not include hidden files in the listing, since hidden files should be...hidden

The 👍's on this comment show that there is some support for having this configurable. As a relative power user on Windows one of the first things I do is to tick the Show Hidden Files box in the Explorer config.

I'm not advocating it in this PR though - I think it deserves a PR of its own if anyone is interested enough. I'll be happy to just get this working on Windows Containers.

On Windows `os.stat` treats host mapped volumes as broken symlinks
@dhirschfeld
Copy link
Contributor Author

The latest push fixes the error when trying to start the server in a host mapped directory - #2574 (comment)

As it is, this PR allows me to use the Notebook/JupyterLab on NanoServer containers.

In this version I've deleted the broken symlink checking from the is_file_hidden_win function as I think the current behaviour of pretending it doesn't exist isn't helpful. Otherwise the change to using lstat is still protected by the WINDOWS_CONTAINER flag. If you think it's better to just always use lstat on Windows I'm happy to make that change but will let a core developer make that call.

If possible it would be great to get this into the next release as without these changes you can't use the Notebook/JupyterLab for anything serious on Windows Containers as there's no way to persist any user data outside of the container.

cc: @minrk, @takluyver

# stat may fail on Windows junctions or non-user-readable dirs
return True

win32_FILE_ATTRIBUTE_HIDDEN = 0x02
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Made the module level global local to this function as it isn't used elsewhere

os.stat(abs_path)
except OSError:
# stat may fail on Windows junctions or non-user-readable dirs
return True
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed checking for a broken symlink as I think it's better to let the user know and have them deal with it rather than pretend it doesn't exist

continue

if not stat.S_ISREG(st.st_mode) and not stat.S_ISDIR(st.st_mode):
if (not WINDOWS_CONTAINER
Copy link
Member

Choose a reason for hiding this comment

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

just win32 instead of WINDOWS_CONTAINER?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As it stands the WINDOWS_CONTAINER flag both tests that the platform is win32 and that you're running from inside a container by checking for the default container user ContainerAdministrator.

It's certainly possible to make the changes for all windows users but I was hesitant to potentially break existing code.

Copy link
Member

Choose a reason for hiding this comment

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

I certainly appreciate aiming for minimal impact. I'm coming from the perspective that anything WINDOWS_CONTAINER specific is going to get minimal testing, so if we can reduce the number of cases it will be easier for us to maintain / catch regressions. Plus, I think showing broken links should be fine, as long as it doesn't cause an error in directory listing (using lstat fixes this, I believe).

Instead of gating on Windows, since it can be a link, maybe it should be adding stat.S_ISLNK to the checks instead of making it conditional on the platform. Does that return True for these mounts? What is os.stat(path).st_mode for these mounts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

os.stat(path) would throw a FileNotFound error. You'd need to either use lstat or pass the follow_symlinks=False arg (py3 only).

lstat gives a 41471 mode:

[20]: st = os.stat(r"C:\jupyter\notebooks", follow_symlinks=False)
... : st
Out[20]: os.stat_result(st_mode=41471, st_ino=281474976710694, st_dev=113970060, st_nlink=1, st_uid=0, st_gid=0, st_size=0, st_atime=1497345137, st_mtime=1497345137, st_ctime=1497345137)

[21]: st.st_file_attributes
Out[21]: 1040

If we don't care about checking if a symlink is broken we could just globally use lstat. The only other issue is then the check for a regular file:

if not stat.S_ISREG(st.st_mode) and not stat.S_ISDIR(st.st_mode):
    self.log.debug("%s not a regular file", os_path)
    continue

In that case doesn't recognise the mapped volume as a directory but sees it as a link instead:

In [2]: stat.S_ISREG(41471)
Out[2]: False

In [3]: stat.S_ISDIR(41471)
Out[3]: False

In [4]: stat.S_ISLNK(41471)
Out[4]: True

...so the if not WINDOWS_CONTAINER could be replaced with and not stat.S_ISLNK(st.st_mode)

Happy to go ahead and make those changes if you think that's the right way forward...

@dhirschfeld
Copy link
Contributor Author

The CI error is below:

  File "/home/travis/build/jupyter/notebook/notebook/services/contents/tests/test_manager.py", line 123, in test_bad_symlink
    self.assertEqual(model['content'], [file_model])
AssertionError: Lists differ: [{'mimetype': 'text/plain', 'w... != [{'mimetype': 'text/plain', 'w...

The function which failed is:

    @dec.skip_win32
    def test_bad_symlink(self):
        with TemporaryDirectory() as td:
            cm = FileContentsManager(root_dir=td)
            path = 'test bad symlink'
            _make_dir(cm, path)

            file_model = cm.new_untitled(path=path, ext='.txt')

            # create a broken symlink
            self.symlink(cm, "target", '%s/%s' % (path, 'bad symlink'))
            model = cm.get(path)
            self.assertEqual(model['content'], [file_model])

IIUC this function is testing that if you create a broken symlink the FileContentsManager ignores it - i.e pretends it never existed. The changes in the PR deliberately break that behaviour so that the FileContentsManager will list broken symlinks and an OSError will only be thrown later if one attempts to use the directory. So, if these changes get the go-ahead the test will also need to be changed... if I've got it correct.

@minrk
Copy link
Member

minrk commented Jun 20, 2017

I think you are exactly right. Since we're changing how we treat bad symlinks (they show up as regular files, then 404 when one tries to open them), updating the test is appropriate.

If the contents manager sees your mounts as bad symlinks, do they look like files in the file listing, or directories? Can you open them?

@dhirschfeld
Copy link
Contributor Author

Host mounted volumes have a directory icon as they should. Opening them in the JupyterLab File Browser works fine and correctly lists the contents. It's just that stat fails for whatever reason on them. Not sure if that's a python issue or expected/correct behaviour.

In contrast a broken symlink directory shows up as a directory icon (in this PR) but double-clicking on it in the JupyterLab File Browser pops up an Internal Server Error dialog in the app and logs a FileNotFound error in the terminal.

I'll look to finish off this PR by updating the test for the new behaviour...

Thanks for the review @minrk!

@minrk minrk added this to the 5.1 milestone Jun 20, 2017
self.assertEqual(cp_subdir, os.path.join(root, subd, cpm.checkpoint_dir, cp_name))

@dec.skip_win32
@dec.skipif(sys.platform == 'win32' and sys.version_info[0] < 3)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Prior to version 3.2 os.symlink which is required for this test only existed for unix. This enables testing on Python 3 for Windows as all supported versions have this functionality now.

Copy link
Member

Choose a reason for hiding this comment

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

os.symlink exists now, but I think that in most situations it still raises a permission error, because normal users don't have the right to create symlinks. So I suspect that when we next do a build on Windows, this might fail.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wondered if that might be the case but I figured if the tests passed on appveyor it would be fine.

That was based on the assumption that most normal Windows users probably wouldn't be running the tests. In that case it would only affect a Windows developer trying to run the tests without admin privileges where it would fail with a permission error. I think that having some testing on Windows/appveyor is probably worth any hassle that might cause?

OT: In the future it should be possible for developers without admin privileges to create symlinks on Windows 10 - https://blogs.windows.com/buildingapps/2016/12/02/symlinks-windows-10/#oWkhfiQvGP6wte4V.97

Copy link
Member

Choose a reason for hiding this comment

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

Ah, nice. I guess our build servers have got the 'creators update' and therefore are able to create symlinks. :-)

@dhirschfeld
Copy link
Contributor Author

For reference, this seems like a bug in docker. I still prefer the new behaviour though as hiding errors from the user doesn't give them a chance to do anything about it.

moby/moby#31089
https://go-review.googlesource.com/c/41834/

@takluyver takluyver mentioned this pull request Dec 20, 2017
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Apr 6, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants