/*
* Copyright 2017-present Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.facebook.buck.io.windowspipe;
import com.facebook.buck.io.Transport;
import com.sun.jna.Memory;
import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinError;
import com.sun.jna.platform.win32.WinNT;
import com.sun.jna.platform.win32.WinNT.HANDLE;
import com.sun.jna.ptr.IntByReference;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
/** Implements a {@link Transport} backed by a windows named pipe under the hood. */
public class WindowsNamedPipe implements Transport {
/*
* Implementation notice:
* Blocking (or sync) win api for pipes doesn't allow to read and write at the same time from
* different threads. This may lead to surprising deadlocks in java world.
* So, this WindowsNamedPipe implements UNIX-style blocking IO (when reading and writing
* at the same time is OK) atop of async (or overlapped) win api.
* The main trick is to use `GetOverlappedResult` with wait=true.
*/
private static final WindowsNamedPipeLibrary api = WindowsNamedPipeLibrary.INSTANCE;
private final HANDLE pipeHandle;
private final InputStream in;
private final OutputStream out;
// Reading and writing may happen in different threads in general.
// So, each operation (read, write) has its own waitable object.
private final HANDLE readerWaitable;
private final HANDLE writerWaitable;
/** Creates a Windows named pipe bound to a path */
public static WindowsNamedPipe createPipeWithPath(String path) throws IOException {
HANDLE pipeHandle =
api.CreateFile(
path,
WinNT.GENERIC_READ | WinNT.GENERIC_WRITE,
0,
null,
WinNT.OPEN_EXISTING,
WinNT.FILE_FLAG_OVERLAPPED,
null);
if (WinNT.INVALID_HANDLE_VALUE.equals(pipeHandle)) {
throw new IOException(
"Failed to open a named pipe " + path + " error: " + api.GetLastError());
}
return new WindowsNamedPipe(pipeHandle, createEvent(), createEvent());
}
private WindowsNamedPipe(HANDLE pipeHandle, HANDLE readerWaitable, HANDLE writerWaitable) {
this.pipeHandle = pipeHandle;
this.readerWaitable = readerWaitable;
this.writerWaitable = writerWaitable;
this.in = new NamedPipeInputStream();
this.out = new NamedPipeOutputStream();
}
@Override
public void close() throws IOException {
api.CloseHandle(pipeHandle);
api.CloseHandle(readerWaitable);
api.CloseHandle(writerWaitable);
}
@Override
public InputStream getInputStream() {
return in;
}
@Override
public OutputStream getOutputStream() {
return out;
}
private class NamedPipeOutputStream extends OutputStream {
@Override
public void write(int b) throws IOException {
write(new byte[] {(byte) (0xFF & b)});
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
Pointer lpOverlapped = createOverlapped(writerWaitable).getPointer();
boolean immediate =
api.WriteFile(pipeHandle, ByteBuffer.wrap(b, off, len), len, null, lpOverlapped);
if (!immediate && api.GetLastError() != WinError.ERROR_IO_PENDING) {
throw new IOException("WriteFile() failed. WinError: " + api.GetLastError());
}
IntByReference written = new IntByReference();
// wait = true, blocked until data is written
if (!api.GetOverlappedResult(pipeHandle, lpOverlapped, written, true)) {
throw new IOException("GetOverlappedResult() failed for write operation");
}
if (written.getValue() != len) {
throw new IOException("WriteFile() wrote less bytes than requested");
}
}
}
private class NamedPipeInputStream extends InputStream {
@Override
public int read() throws IOException {
byte[] b = new byte[1];
int read = read(b);
return read == -1 ? -1 : 0xFF & b[0];
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
// Memory is used because the actual result is taken after the call to GetOverlappedResult.
// ByteBuffer doesn't work here, since JNA ByteBuffer magic doesn't go beyond a single call.
Memory readBuffer = new Memory(len);
Pointer lpOverlapped = createOverlapped(readerWaitable).getPointer();
boolean immediate = api.ReadFile(pipeHandle, readBuffer, len, null, lpOverlapped);
if (!immediate && api.GetLastError() != WinError.ERROR_IO_PENDING) {
throw new IOException("ReadFile() failed. WinError: " + api.GetLastError());
}
IntByReference read = new IntByReference();
// wait = true, blocked until data is read
if (!api.GetOverlappedResult(pipeHandle, lpOverlapped, read, true)) {
throw new IOException("GetOverlappedResult() failed for read operation");
}
int readValue = read.getValue();
if (readValue == 0) {
return -1;
}
readBuffer.read(0, b, off, readValue);
return readValue;
}
}
private static HANDLE createEvent() throws IOException {
HANDLE event = api.CreateEvent(null, true, false, null);
if (event == null) {
throw new IOException("CreateEvent() failed. Error: " + api.GetLastError());
}
return event;
}
private static WinBase.OVERLAPPED createOverlapped(WinNT.HANDLE event) {
WinBase.OVERLAPPED olap = new WinBase.OVERLAPPED();
olap.hEvent = event;
olap.write();
return olap;
}
}