Skip to content

Commit 5fbf85f

Browse files
authored
clipboard,win32: rewrite clipboard functions to handle errors (#33831)
Per comment (although it failed to note that we were possibly previously always leaking the memory).
1 parent b0431ce commit 5fbf85f

File tree

2 files changed

+95
-43
lines changed

2 files changed

+95
-43
lines changed

stdlib/InteractiveUtils/src/clipboard.jl

Lines changed: 68 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ if Sys.isapple()
1818
open(pipeline(pbcopy_cmd, stderr=stderr), "w") do io
1919
print(io, x)
2020
end
21+
nothing
2122
end
2223
function clipboard()
2324
pbpaste_cmd = `pbpaste`
@@ -26,89 +27,113 @@ if Sys.isapple()
2627
if Sys.which("reattach-to-user-namespace") !== nothing
2728
pbcopy_cmd = `reattach-to-user-namespace pbpaste`
2829
end
29-
read(pbpaste_cmd, String)
30+
return read(pbpaste_cmd, String)
3031
end
3132

3233
elseif Sys.islinux() || Sys.KERNEL === :FreeBSD
3334
_clipboardcmd = nothing
34-
const _clipboardcmds = Dict(
35-
:copy => Dict(
35+
const _clipboard_copy = Dict(
3636
:xsel => Sys.islinux() ?
37-
`xsel --nodetach --input --clipboard` : `xsel -c`,
37+
`xsel --nodetach --input --clipboard` :
38+
`xsel -c`,
3839
:xclip => `xclip -silent -in -selection clipboard`,
39-
),
40-
:paste => Dict(
40+
)
41+
const _clipboard_paste = Dict(
4142
:xsel => Sys.islinux() ?
42-
`xsel --nodetach --output --clipboard` : `xsel -p`,
43+
`xsel --nodetach --output --clipboard` :
44+
`xsel -p`,
4345
:xclip => `xclip -quiet -out -selection clipboard`,
4446
)
45-
)
4647
function clipboardcmd()
4748
global _clipboardcmd
4849
_clipboardcmd !== nothing && return _clipboardcmd
4950
for cmd in (:xclip, :xsel)
5051
success(pipeline(`which $cmd`, devnull)) && return _clipboardcmd = cmd
5152
end
52-
pkgs = @static if Sys.islinux()
53-
"xsel or xclip"
54-
elseif Sys.KERNEL === :FreeBSD
53+
pkgs = @static if Sys.KERNEL === :FreeBSD
5554
"x11/xsel or x11/xclip"
55+
else
56+
"xsel or xclip"
5657
end
5758
error("no clipboard command found, please install $pkgs")
5859
end
5960
function clipboard(x)
6061
c = clipboardcmd()
61-
cmd = get(_clipboardcmds[:copy], c, nothing)
62-
if cmd === nothing
63-
error("unexpected clipboard command: $c")
64-
end
62+
cmd = _clipboard_copy[c]
6563
open(pipeline(cmd, stderr=stderr), "w") do io
6664
print(io, x)
6765
end
66+
nothing
6867
end
6968
function clipboard()
7069
c = clipboardcmd()
71-
cmd = get(_clipboardcmds[:paste], c, nothing)
72-
if cmd === nothing
73-
error("unexpected clipboard command: $c")
74-
end
75-
read(pipeline(cmd, stderr=stderr), String)
70+
cmd = _clipboardcmds_paste[c]
71+
return read(pipeline(cmd, stderr=stderr), String)
7672
end
7773

7874
elseif Sys.iswindows()
79-
# TODO: these functions leak memory and memory locks if they throw an error
8075
function clipboard(x::AbstractString)
8176
if Base.containsnul(x)
8277
throw(ArgumentError("Windows clipboard strings cannot contain NUL character"))
8378
end
84-
Base.windowserror(:OpenClipboard, 0==ccall((:OpenClipboard, "user32"), stdcall, Cint, (Ptr{Cvoid},), C_NULL))
85-
Base.windowserror(:EmptyClipboard, 0==ccall((:EmptyClipboard, "user32"), stdcall, Cint, ()))
8679
x_u16 = Base.cwstring(x)
80+
pdata = Ptr{UInt16}(C_NULL)
81+
function cleanup(cause)
82+
errno = cause == :success ? UInt32(0) : Libc.GetLastError()
83+
if cause !== :OpenClipboard
84+
if cause !== :success && pdata != C_NULL
85+
ccall((:GlobalFree, "kernel32"), stdcall, Cint, (Ptr{UInt16},), pdata)
86+
end
87+
ccall((:CloseClipboard, "user32"), stdcall, Cint, ()) == 0 && Base.windowserror(:CloseClipboard) # this should never fail
88+
end
89+
cause == :success || Base.windowserror(cause, errno)
90+
nothing
91+
end
92+
ccall((:OpenClipboard, "user32"), stdcall, Cint, (Ptr{Cvoid},), C_NULL) == 0 && return Base.windowserror(:OpenClipboard)
93+
ccall((:EmptyClipboard, "user32"), stdcall, Cint, ()) == 0 && return cleanup(:EmptyClipboard)
8794
# copy data to locked, allocated space
88-
p = ccall((:GlobalAlloc, "kernel32"), stdcall, Ptr{UInt16}, (UInt16, Int32), 2, sizeof(x_u16))
89-
Base.windowserror(:GlobalAlloc, p==C_NULL)
90-
plock = ccall((:GlobalLock, "kernel32"), stdcall, Ptr{UInt16}, (Ptr{UInt16},), p)
91-
Base.windowserror(:GlobalLock, plock==C_NULL)
92-
ccall(:memcpy, Ptr{UInt16}, (Ptr{UInt16},Ptr{UInt16},Int), plock, x_u16, sizeof(x_u16))
93-
Base.windowserror(:GlobalUnlock, 0==ccall((:GlobalUnlock, "kernel32"), stdcall, Cint, (Ptr{Cvoid},), plock))
94-
pdata = ccall((:SetClipboardData, "user32"), stdcall, Ptr{UInt16}, (UInt32, Ptr{UInt16}), 13, p)
95-
Base.windowserror(:SetClipboardData, pdata!=p)
96-
ccall((:CloseClipboard, "user32"), stdcall, Cvoid, ())
95+
pdata = ccall((:GlobalAlloc, "kernel32"), stdcall, Ptr{UInt16}, (Cuint, Csize_t), 2 #=GMEM_MOVEABLE=#, sizeof(x_u16))
96+
pdata == C_NULL && return cleanup(:GlobalAlloc)
97+
plock = ccall((:GlobalLock, "kernel32"), stdcall, Ptr{UInt16}, (Ptr{UInt16},), pdata)
98+
plock == C_NULL && return cleanup(:GlobalLock)
99+
ccall(:memcpy, Ptr{UInt16}, (Ptr{UInt16}, Ptr{UInt16}, Csize_t), plock, x_u16, sizeof(x_u16))
100+
unlock = ccall((:GlobalUnlock, "kernel32"), stdcall, Cint, (Ptr{UInt16},), pdata)
101+
(unlock == 0 && Libc.GetLastError() == 0) || return cleanup(:GlobalUnlock) # this should never fail
102+
pset = ccall((:SetClipboardData, "user32"), stdcall, Ptr{UInt16}, (Cuint, Ptr{UInt16}), 13, pdata)
103+
pdata != pset && return cleanup(:SetClipboardData)
104+
cleanup(:success)
97105
end
98106
clipboard(x) = clipboard(sprint(print, x)::String)
99107
function clipboard()
100-
Base.windowserror(:OpenClipboard, 0==ccall((:OpenClipboard, "user32"), stdcall, Cint, (Ptr{Cvoid},), C_NULL))
101-
pdata = ccall((:GetClipboardData, "user32"), stdcall, Ptr{UInt16}, (UInt32,), 13)
102-
Base.windowserror(:GetClipboardData, pdata==C_NULL)
103-
Base.windowserror(:CloseClipboard, 0==ccall((:CloseClipboard, "user32"), stdcall, Cint, ()))
108+
function cleanup(cause)
109+
errno = cause == :success ? UInt32(0) : Libc.GetLastError()
110+
if cause !== :OpenClipboard
111+
ccall((:CloseClipboard, "user32"), stdcall, Cint, ()) == 0 && Base.windowserror(:CloseClipboard) # this should never fail
112+
end
113+
if cause !== :success && (cause !== :GetClipboardData || errno != 0)
114+
Base.windowserror(cause, errno)
115+
end
116+
""
117+
end
118+
ccall((:OpenClipboard, "user32"), stdcall, Cint, (Ptr{Cvoid},), C_NULL) == 0 && return Base.windowserror(:OpenClipboard)
119+
ccall(:SetLastError, stdcall, Cvoid, (UInt32,), 0) # allow distinguishing if the clipboard simply didn't have text
120+
pdata = ccall((:GetClipboardData, "user32"), stdcall, Ptr{UInt16}, (Cuint,), 13)
121+
pdata == C_NULL && return cleanup(:GetClipboardData)
104122
plock = ccall((:GlobalLock, "kernel32"), stdcall, Ptr{UInt16}, (Ptr{UInt16},), pdata)
105-
Base.windowserror(:GlobalLock, plock==C_NULL)
106-
# find NUL terminator (0x0000 16-bit code unit)
107-
len = 0
108-
while unsafe_load(plock, len+1) != 0; len += 1; end
109-
# get Vector{UInt16}, transcode data to UTF-8, make a String of it
110-
s = transcode(String, unsafe_wrap(Array, plock, len))
111-
Base.windowserror(:GlobalUnlock, 0==ccall((:GlobalUnlock, "kernel32"), stdcall, Cint, (Ptr{UInt16},), plock))
123+
plock == C_NULL && return cleanup(:GlobalLock)
124+
s = try
125+
# find NUL terminator (0x0000 16-bit code unit)
126+
len = 0
127+
while unsafe_load(plock, len + 1) != 0
128+
len += 1
129+
end
130+
# get Vector{UInt16}, transcode data to UTF-8, make a String of it
131+
transcode(String, unsafe_wrap(Array, plock, len))
132+
finally
133+
unlock = ccall((:GlobalUnlock, "kernel32"), stdcall, Cint, (Ptr{UInt16},), pdata)
134+
(unlock != 0 || Libc.GetLastError() == 0) || return cleanup(:GlobalUnlock) # this should never fail
135+
cleanup(:success)
136+
end
112137
return s
113138
end
114139

stdlib/InteractiveUtils/test/runtests.jl

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,12 +383,39 @@ withenv("JULIA_EDITOR" => nothing, "VISUAL" => nothing, "EDITOR" => nothing) do
383383
end
384384

385385
# clipboard functionality
386+
if Sys.isapple()
387+
let str = "abc\0def"
388+
clipboard(str)
389+
@test clipboard() == str
390+
end
391+
end
386392
if Sys.iswindows() || Sys.isapple()
387393
for str in ("Hello, world.", "∀ x ∃ y", "")
388394
clipboard(str)
389395
@test clipboard() == str
390396
end
391397
end
398+
@static if Sys.iswindows()
399+
@test_broken false # CI has trouble with this test
400+
## concurrent access error
401+
#hDesktop = ccall((:GetDesktopWindow, "user32"), stdcall, Ptr{Cvoid}, ())
402+
#ccall((:OpenClipboard, "user32"), stdcall, Cint, (Ptr{Cvoid},), hDesktop) == 0 && Base.windowserror("OpenClipboard")
403+
#try
404+
# @test_throws Base.SystemError("OpenClipboard", 0, Base.WindowsErrorInfo(0x00000005, nothing)) clipboard() # ACCESS_DENIED
405+
#finally
406+
# ccall((:CloseClipboard, "user32"), stdcall, Cint, ()) == 0 && Base.windowserror("CloseClipboard")
407+
#end
408+
# empty clipboard failure
409+
ccall((:OpenClipboard, "user32"), stdcall, Cint, (Ptr{Cvoid},), C_NULL) == 0 && Base.windowserror("OpenClipboard")
410+
try
411+
ccall((:EmptyClipboard, "user32"), stdcall, Cint, ()) == 0 && Base.windowserror("EmptyClipboard")
412+
finally
413+
ccall((:CloseClipboard, "user32"), stdcall, Cint, ()) == 0 && Base.windowserror("CloseClipboard")
414+
end
415+
@test clipboard() == ""
416+
# nul error (unsupported data)
417+
@test_throws ArgumentError("Windows clipboard strings cannot contain NUL character") clipboard("abc\0")
418+
end
392419

393420
# buildbot path updating
394421
file, ln = functionloc(versioninfo, Tuple{})

0 commit comments

Comments
 (0)