
aQute.bnd.remoteworkspace.server.RemoteWorkspaceServer Maven / Gradle / Ivy
Show all versions of biz.aQute.bndlib Show documentation
package aQute.bnd.remoteworkspace.server;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import aQute.bnd.build.Container;
import aQute.bnd.build.Project;
import aQute.bnd.build.Run;
import aQute.bnd.build.Workspace;
import aQute.bnd.exceptions.Exceptions;
import aQute.bnd.header.Attrs;
import aQute.bnd.header.Parameters;
import aQute.bnd.osgi.About;
import aQute.bnd.osgi.Builder;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.Descriptors.PackageRef;
import aQute.bnd.osgi.Jar;
import aQute.bnd.osgi.Processor;
import aQute.bnd.remoteworkspace.client.RemoteWorkspaceClientFactory;
import aQute.bnd.service.Strategy;
import aQute.bnd.service.remoteworkspace.RemoteWorkspace;
import aQute.bnd.service.remoteworkspace.RemoteWorkspaceClient;
import aQute.bnd.service.specifications.BuilderSpecification;
import aQute.bnd.service.specifications.RunSpecification;
import aQute.bnd.version.Version;
import aQute.bnd.version.VersionRange;
import aQute.lib.aspects.Aspects;
import aQute.lib.io.IO;
import aQute.lib.link.Link;
import aQute.lib.utf8properties.UTF8Properties;
import aQute.libg.glob.Glob;
/**
* Implements an RPC interface to a workspace. When a workspace is created then
* it can create a Remote Workspace Server to allow remote access.
*
* This server will register the ephemeral port it uses in the
* {@code cnf/cache/remotews} directory so that it can be found by clients. This
* registration is deleted when the process properly exits.
*/
public class RemoteWorkspaceServer implements Closeable {
final Logger logger = LoggerFactory.getLogger(RemoteWorkspaceServer.class);
final Closeable server;
final File remotewsPort;
final Workspace workspace;
final ScheduledFuture> registerPort;
private final long startingTime;
/**
* Create a new Remote Workspace Server. This will create a server socket on
* a random port. The protocole over this socket is defined by the
* {@link RemoteWorkspace} interface. The port number will be registered in
* {@code cnf/cache/remotews/}. That is, it is possible to have
* multiple workspaces open on the system workspace. (Locking between
* workspaces is not handled though.)
*
* @param workspace the given workspace
*/
public RemoteWorkspaceServer(Workspace workspace) throws UnknownHostException, IOException {
this.workspace = workspace;
ServerSocket server = new ServerSocket(0, 10, InetAddress.getLoopbackAddress());
RemoteWorkspace workspaceLocker = Aspects.intercept(RemoteWorkspace.class, new Instance())
.around((inv, c) -> workspace.readLocked(c))
.build();
this.server = Link.server("remotews", RemoteWorkspaceClient.class, server, l -> workspaceLocker, true,
Processor.getExecutor());
File remotews = RemoteWorkspaceClientFactory.getPortDirectory(workspace.getBase(), workspace.getBase());
remotews.mkdirs();
if (!remotews.isDirectory())
throw new IllegalStateException(
"Cannot create the remote workspace directory with port numbers " + remotews);
remotewsPort = new File(remotews, server.getLocalPort() + "");
remotewsPort.deleteOnExit();
startingTime = System.currentTimeMillis();
register();
registerPort = Processor.getScheduledExecutor()
.scheduleAtFixedRate(this::register, 2 /* nice for testing */, 5, TimeUnit.SECONDS);
}
void register() {
if (remotewsPort.isFile() && (Math.abs(startingTime - remotewsPort.lastModified()) < 1000L)) {
return;
}
try {
IO.delete(remotewsPort);
IO.mkdirs(remotewsPort.getParentFile());
IO.store(remotewsPort.getName(), remotewsPort);
remotewsPort.setLastModified(startingTime);
logger.info("Registering remote workspace server {}", remotewsPort);
} catch (IOException e) {
logger.warn("Cannot open remote workspace server {} {}", remotewsPort, e);
}
}
/**
* Close the server. This generally happens when the corresponding workspace
* is closed. It will release the ephemeral port and delete the registration
* file.
*/
@Override
public void close() throws IOException {
logger.info("Closing remote workspace server {}", remotewsPort);
registerPort.cancel(false);
IO.delete(remotewsPort);
server.close();
}
/**
* Holds the implementations of the {@link RemoteWorkspace}
*/
class Instance implements RemoteWorkspace {
@Override
public String getBndVersion() {
return About.CURRENT.toString();
}
@Override
public RunSpecification getRun(String pathToBndOrBndrun) {
try {
File file = new File(pathToBndOrBndrun);
if (file.isDirectory()) {
Project project = workspace.getProjectFromFile(file);
if (project == null)
throw new IllegalArgumentException(
"No such project " + pathToBndOrBndrun + " in workspace " + workspace);
return project.getSpecification();
} else {
try (Run run = Run.createRun(workspace, file)) {
return run.getSpecification();
}
}
} catch (Exception e) {
throw Exceptions.duck(e);
}
}
@Override
public List getLatestBundles(String projectDir, String specification) {
try {
Project project = getProject(projectDir);
return Container.toPaths(null, project.getBundles(Strategy.HIGHEST, specification, "remote"));
} catch (Exception e) {
throw Exceptions.duck(e);
}
}
@Override
public RunSpecification analyzeTestSetup(String projectDir) {
try {
Project project = getProject(projectDir);
RunSpecification r = project.getSpecification();
try (Builder a = new Builder()) {
a.setJar(project.getTestOutput());
a.setConditionalPackage("!java.*,*");
a.setProperty(Constants.EXPORT_CONTENTS, "*");
a.addClasspath(project.getOutput());
for (Container c : project.getTestpath()) {
if (c.getError() != null)
r.errors.add("Not a valid testpath entry " + c + " " + c.getError());
else
a.addClasspath(c.getFile());
}
for (Container c : project.getBuildpath()) {
if (c.getError() != null)
r.errors.add("Not a valid buildpath entry " + c + " " + c.getError());
else
a.addClasspath(c.getFile());
}
a.build();
r.errors.addAll(a.getErrors());
String clauses = Processor.printClauses(a.getExports());
Parameters extraPackages = new Parameters(clauses);
r.extraSystemPackages.putAll(extraPackages.toBasic());
return r;
}
} catch (Throwable e) {
throw Exceptions.duck(e);
}
}
@Override
public byte[] build(String projectPath, BuilderSpecification spec) {
try {
File projectDir = new File(projectPath);
Project project = workspace.getProjectFromFile(projectDir);
if (project == null)
throw new IllegalArgumentException("No such project " + projectPath);
try (Builder builder = getBuilder(project, spec.parent)) {
builder.setBase(project.getBase());
Jar build = builder.from(spec)
.build();
if (!builder.isOk()) {
throw new IllegalStateException(builder.getErrors()
.stream()
.collect(Collectors.joining("\n")));
}
if (spec.testBundle != null) {
File[] buildFiles = project.getBuildFiles(false);
if (buildFiles == null)
throw new IllegalStateException(projectPath + ": merge requested but has no build JAR");
Glob glob = new Glob(spec.testBundle);
boolean found = false;
buildfiles: for (File f : buildFiles) {
if (glob.matches(f.getName())) {
found = true;
Jar jar = new Jar(buildFiles[0]);
builder.addClose(jar);
jar.addAll(build);
Manifest manifest = jar.getManifest();
manifest.getMainAttributes()
.putValue("DynamicImport-Package", "*");
merge(manifest, build.getManifest(), Constants.SERVICE_COMPONENT);
jar.setManifest(manifest);
build = jar;
break buildfiles;
}
}
}
ByteArrayOutputStream bout = new ByteArrayOutputStream();
build.write(bout);
return bout.toByteArray();
}
} catch (Exception e) {
throw Exceptions.duck(e);
}
}
private void merge(Manifest a, Manifest b, String hdr) {
if (a == null)
return;
if (b == null)
return;
String aa = a.getMainAttributes()
.getValue(hdr);
String bb = b.getMainAttributes()
.getValue(hdr);
if (aa == null && bb == null)
return;
if (bb == null)
return;
if (aa == null) {
a.getMainAttributes()
.putValue(hdr, bb);
return;
}
String cc = aa + "," + bb;
a.getMainAttributes()
.putValue(hdr, cc);
}
/**
* Calculate the builder with the properly inheritance structure.
*
* @param project The context project
* @param parent The comma separated parent string. The last entry may
* be either PROJECT or WORKSPACE
*/
private Builder getBuilder(Project project, List parent) throws Exception {
if (parent == null || parent.isEmpty())
return new Builder();
Builder builder = new Builder();
builder.setBase(project.getBase());
List paths = new ArrayList<>(parent);
String last = paths.get(paths.size() - 1);
boolean workspaceParent = BuilderSpecification.WORKSPACE.equals(last);
boolean projectParent = BuilderSpecification.PROJECT.equals(last);
if (workspaceParent || projectParent) {
paths.remove(paths.size() - 1);
if (projectParent)
builder.setParent(project);
else if (workspaceParent)
builder.setParent(project.getWorkspace());
}
workspaceParent = paths.remove(BuilderSpecification.WORKSPACE);
projectParent = paths.remove(BuilderSpecification.PROJECT);
if (workspaceParent || projectParent) {
builder.error("PROJECT or WORKSPACE parent can only be specified as the last entry");
}
// earlier entries are prioritized
Collections.reverse(paths);
paths.forEach(path -> {
File file = new File(path);
try {
if (file.isFile()) {
UTF8Properties p = new UTF8Properties();
p.load(file, builder);
builder.setProperties(p);
} else
builder.error("specified file %s as parent for build but no such file exist", file.toString());
} catch (Exception e) {
builder.exception(e, "Reading properties %s", file);
}
});
return builder;
}
void doPackage(Parameters extraPackages, PackageRef p, Attrs a) {
Attrs attrs = new Attrs(a);
String v = attrs.getVersion();
if (v != null) {
VersionRange vr = VersionRange.parseOSGiVersionRange(v);
Version version = vr.getLow();
attrs.put("version", version.toString());
}
extraPackages.put(p.getFQN(), attrs);
}
Project getProject(String projectDir) {
File dir = new File(projectDir);
if (!dir.isDirectory())
throw new IllegalArgumentException("Not a directory " + projectDir);
Project project = workspace.getProjectFromFile(dir);
if (project == null || !project.isValid())
throw new IllegalArgumentException("Not a valid project directory " + projectDir);
return project;
}
@Override
public List getProjects() {
try {
return workspace.getAllProjects()
.stream()
.map(Project::toString)
.collect(Collectors.toList());
} catch (Exception e) {
throw Exceptions.duck(e);
}
}
@Override
public void close() throws IOException {}
}
}