| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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. |
| */ |
| // DWB8: throw IllegatlStateException if session used after closed (as per rfc132) |
| // DWB9: there is no API to list all variables: https://www.osgi.org/bugzilla/show_bug.cgi?id=49 |
| // DWB10: add SCOPE support: https://www.osgi.org/bugzilla/show_bug.cgi?id=51 |
| package org.apache.felix.gogo.runtime; |
| |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.io.PrintStream; |
| import java.lang.reflect.Method; |
| import java.lang.reflect.Modifier; |
| import java.nio.channels.Channel; |
| import java.nio.channels.Channels; |
| import java.nio.channels.ReadableByteChannel; |
| import java.nio.channels.WritableByteChannel; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Dictionary; |
| import java.util.Enumeration; |
| import java.util.Formatter; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.ConcurrentMap; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.Future; |
| |
| import org.apache.felix.service.command.Job; |
| import org.apache.felix.service.command.Job.Status; |
| import org.apache.felix.service.command.JobListener; |
| import org.apache.felix.service.command.Process; |
| import org.apache.felix.gogo.runtime.Pipe.Result; |
| import org.apache.felix.service.command.CommandProcessor; |
| import org.apache.felix.service.command.CommandSession; |
| import org.apache.felix.service.command.Converter; |
| import org.apache.felix.service.command.Function; |
| import org.apache.felix.service.threadio.ThreadIO; |
| |
| public class CommandSessionImpl implements CommandSession, Converter |
| { |
| public static final String SESSION_CLOSED = "session is closed"; |
| public static final String VARIABLES = ".variables"; |
| public static final String COMMANDS = ".commands"; |
| public static final String CONSTANTS = ".constants"; |
| private static final String COLUMN = "%-20s %s\n"; |
| |
| // Streams and channels |
| protected InputStream in; |
| protected OutputStream out; |
| protected PrintStream pout; |
| protected OutputStream err; |
| protected PrintStream perr; |
| protected Channel[] channels; |
| |
| private final CommandProcessorImpl processor; |
| protected final ConcurrentMap<String, Object> variables = new ConcurrentHashMap<>(); |
| private volatile boolean closed; |
| private final List<JobImpl> jobs = new ArrayList<>(); |
| private JobListener jobListener; |
| |
| private final ExecutorService executor; |
| |
| private Path currentDir; |
| private ClassLoader classLoader; |
| |
| protected CommandSessionImpl(CommandProcessorImpl shell, CommandSessionImpl parent) |
| { |
| this.currentDir = parent.currentDir; |
| this.executor = Executors.newCachedThreadPool(ThreadUtils.namedThreadFactory("session")); |
| this.processor = shell; |
| this.channels = parent.channels; |
| this.in = parent.in; |
| this.out = parent.out; |
| this.err = parent.err; |
| this.pout = parent.pout; |
| this.perr = parent.perr; |
| } |
| |
| protected CommandSessionImpl(CommandProcessorImpl shell, InputStream in, OutputStream out, OutputStream err) |
| { |
| this.currentDir = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize(); |
| this.executor = Executors.newCachedThreadPool(ThreadUtils.namedThreadFactory("session")); |
| this.processor = shell; |
| ReadableByteChannel inCh = Channels.newChannel(in); |
| WritableByteChannel outCh = Channels.newChannel(out); |
| WritableByteChannel errCh = out == err ? outCh : Channels.newChannel(err); |
| this.channels = new Channel[] { inCh, outCh, errCh }; |
| this.in = in; |
| this.out = out; |
| this.err = err; |
| this.pout = out instanceof PrintStream ? (PrintStream) out : new PrintStream(out, true); |
| this.perr = out == err ? pout : err instanceof PrintStream ? (PrintStream) err : new PrintStream(err, true); |
| } |
| |
| ThreadIO threadIO() |
| { |
| return processor.threadIO; |
| } |
| |
| public CommandProcessor processor() |
| { |
| return processor; |
| } |
| |
| public ConcurrentMap<String, Object> getVariables() |
| { |
| return variables; |
| } |
| |
| public Path currentDir() |
| { |
| return currentDir; |
| } |
| |
| public void currentDir(Path path) |
| { |
| currentDir = path; |
| } |
| |
| public ClassLoader classLoader() |
| { |
| return classLoader != null ? classLoader : getClass().getClassLoader(); |
| } |
| |
| public void classLoader(ClassLoader classLoader) |
| { |
| this.classLoader = classLoader; |
| } |
| |
| public void close() |
| { |
| if (!this.closed) |
| { |
| this.closed = true; |
| this.processor.closeSession(this); |
| executor.shutdownNow(); |
| } |
| } |
| |
| public Object execute(CharSequence commandline) throws Exception |
| { |
| assert processor != null; |
| |
| if (closed) |
| { |
| throw new IllegalStateException(SESSION_CLOSED); |
| } |
| |
| processor.beforeExecute(this, commandline); |
| |
| try |
| { |
| Closure impl = new Closure(this, null, commandline); |
| Object result = impl.execute(this, null); |
| processor.afterExecute(this, commandline, result); |
| return result; |
| } |
| catch (Exception e) |
| { |
| processor.afterExecute(this, commandline, e); |
| throw e; |
| } |
| } |
| |
| public InputStream getKeyboard() |
| { |
| return in; |
| } |
| |
| public Object get(String name) |
| { |
| // there is no API to list all variables, so overload name == null |
| if (name == null || VARIABLES.equals(name)) |
| { |
| return Collections.unmodifiableSet(variables.keySet()); |
| } |
| |
| if (COMMANDS.equals(name)) |
| { |
| return processor.getCommands(); |
| } |
| |
| if (CONSTANTS.equals(name)) |
| { |
| return Collections.unmodifiableSet(processor.constants.keySet()); |
| } |
| |
| Object val = processor.constants.get(name); |
| if (val != null) |
| { |
| return val; |
| } |
| |
| val = variables.get("#" + name); |
| if (val instanceof Function) |
| { |
| try |
| { |
| val = ((Function) val).execute(this, null); |
| } |
| catch (Exception e) |
| { |
| // Ignore |
| } |
| return val; |
| } |
| else if (val != null) |
| { |
| return val; |
| } |
| |
| val = variables.get(name); |
| if (val != null) |
| { |
| return val; |
| } |
| |
| return processor.getCommand(name, variables.get("SCOPE")); |
| } |
| |
| public Object put(String name, Object value) |
| { |
| if (value != null) |
| { |
| return variables.put(name, value); |
| } |
| else |
| { |
| return variables.remove(name); |
| } |
| } |
| |
| public PrintStream getConsole() |
| { |
| return pout; |
| } |
| |
| @SuppressWarnings("unchecked") |
| public CharSequence format(Object target, int level, Converter escape) throws Exception |
| { |
| if (target == null) |
| { |
| return "null"; |
| } |
| |
| if (target instanceof CharSequence) |
| { |
| return (CharSequence) target; |
| } |
| |
| for (Converter c : processor.converters) |
| { |
| CharSequence s = c.format(target, level, this); |
| if (s != null) |
| { |
| return s; |
| } |
| } |
| |
| if (target.getClass().isArray()) |
| { |
| if (target.getClass().getComponentType().isPrimitive()) |
| { |
| if (target.getClass().getComponentType() == boolean.class) |
| { |
| return Arrays.toString((boolean[]) target); |
| } |
| else |
| { |
| if (target.getClass().getComponentType() == byte.class) |
| { |
| return Arrays.toString((byte[]) target); |
| } |
| else |
| { |
| if (target.getClass().getComponentType() == short.class) |
| { |
| return Arrays.toString((short[]) target); |
| } |
| else |
| { |
| if (target.getClass().getComponentType() == int.class) |
| { |
| return Arrays.toString((int[]) target); |
| } |
| else |
| { |
| if (target.getClass().getComponentType() == long.class) |
| { |
| return Arrays.toString((long[]) target); |
| } |
| else |
| { |
| if (target.getClass().getComponentType() == float.class) |
| { |
| return Arrays.toString((float[]) target); |
| } |
| else |
| { |
| if (target.getClass().getComponentType() == double.class) |
| { |
| return Arrays.toString((double[]) target); |
| } |
| else |
| { |
| if (target.getClass().getComponentType() == char.class) |
| { |
| return Arrays.toString((char[]) target); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| target = Arrays.asList((Object[]) target); |
| } |
| if (target instanceof Collection) |
| { |
| if (level == Converter.INSPECT) |
| { |
| StringBuilder sb = new StringBuilder(); |
| Collection<?> c = (Collection<?>) target; |
| for (Object o : c) |
| { |
| sb.append(format(o, level + 1, this)); |
| sb.append("\n"); |
| } |
| return sb; |
| } |
| else |
| { |
| if (level == Converter.LINE) |
| { |
| StringBuilder sb = new StringBuilder(); |
| Collection<?> c = (Collection<?>) target; |
| sb.append("["); |
| for (Object o : c) |
| { |
| if (sb.length() > 1) |
| { |
| sb.append(", "); |
| } |
| sb.append(format(o, level + 1, this)); |
| } |
| sb.append("]"); |
| return sb; |
| } |
| } |
| } |
| if (target instanceof Dictionary) |
| { |
| Map<Object, Object> result = new HashMap<>(); |
| for (Enumeration<Object> e = ((Dictionary<Object, Object>) target).keys(); e.hasMoreElements();) |
| { |
| Object key = e.nextElement(); |
| result.put(key, ((Dictionary<Object, Object>) target).get(key)); |
| } |
| target = result; |
| } |
| if (target instanceof Map) |
| { |
| if (level == Converter.INSPECT) |
| { |
| StringBuilder sb = new StringBuilder(); |
| Map<?, ?> c = (Map<?, ?>) target; |
| for (Map.Entry<?, ?> entry : c.entrySet()) |
| { |
| CharSequence key = format(entry.getKey(), level + 1, this); |
| sb.append(key); |
| for (int i = key.length(); i < 20; i++) |
| { |
| sb.append(' '); |
| } |
| sb.append(format(entry.getValue(), level + 1, this)); |
| sb.append("\n"); |
| } |
| return sb; |
| } |
| else |
| { |
| if (level == Converter.LINE) |
| { |
| StringBuilder sb = new StringBuilder(); |
| Map<?, ?> c = (Map<?, ?>) target; |
| sb.append("["); |
| for (Map.Entry<?, ?> entry : c.entrySet()) |
| { |
| if (sb.length() > 1) |
| { |
| sb.append(", "); |
| } |
| sb.append(format(entry, level + 1, this)); |
| } |
| sb.append("]"); |
| return sb; |
| } |
| } |
| } |
| if (target instanceof Path) |
| { |
| return target.toString(); |
| } |
| if (level == Converter.INSPECT) |
| { |
| return inspect(target); |
| } |
| else |
| { |
| return target.toString(); |
| } |
| } |
| |
| CharSequence inspect(Object b) |
| { |
| boolean found = false; |
| try (Formatter f = new Formatter();) |
| { |
| Method methods[] = b.getClass().getMethods(); |
| for (Method m : methods) |
| { |
| try |
| { |
| String name = m.getName(); |
| if (m.getName().startsWith("get") && !m.getName().equals("getClass") && m.getParameterTypes().length == 0 && Modifier.isPublic(m.getModifiers())) |
| { |
| found = true; |
| name = name.substring(3); |
| m.setAccessible(true); |
| Object value = m.invoke(b, (Object[]) null); |
| f.format(COLUMN, name, format(value, Converter.LINE, this)); |
| } |
| } |
| catch (Exception e) |
| { |
| // Ignore |
| } |
| } |
| if (found) |
| { |
| return (StringBuilder) f.out(); |
| } |
| else |
| { |
| return b.toString(); |
| } |
| } |
| } |
| |
| public Object convert(Class<?> desiredType, Object in) |
| { |
| return processor.convert(this, desiredType, in); |
| } |
| |
| public Object doConvert(Class<?> desiredType, Object in) |
| { |
| if (desiredType == Class.class) |
| { |
| try |
| { |
| return Class.forName(in.toString(), true, classLoader()); |
| } |
| catch (ClassNotFoundException e) |
| { |
| return null; |
| } |
| } |
| return processor.doConvert(desiredType, in); |
| } |
| |
| public CharSequence format(Object result, int inspect) |
| { |
| try |
| { |
| return format(result, inspect, this); |
| } |
| catch (Exception e) |
| { |
| return "<can not format " + result + ":" + e; |
| } |
| } |
| |
| public Object expr(CharSequence expr) |
| { |
| return processor.expr(this, expr); |
| } |
| |
| public Object invoke(Object target, String name, List<Object> args) throws Exception |
| { |
| return processor.invoke(this, target, name, args); |
| } |
| |
| public Path redirect(Path path, int mode) |
| { |
| return processor.redirect(this, path, mode); |
| } |
| |
| @Override |
| public List<Job> jobs() |
| { |
| synchronized (jobs) |
| { |
| return Collections.<Job>unmodifiableList(jobs); |
| } |
| } |
| |
| public static JobImpl currentJob() |
| { |
| return (JobImpl) Job.Utils.current(); |
| } |
| |
| @Override |
| public JobImpl foregroundJob() |
| { |
| List<JobImpl> jobs; |
| synchronized (this.jobs) |
| { |
| jobs = new ArrayList<>(this.jobs); |
| } |
| for (JobImpl j : jobs) { |
| if (j.parent == null && j.status() == Status.Foreground) { |
| return j; |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| public void setJobListener(JobListener listener) |
| { |
| synchronized (jobs) |
| { |
| jobListener = listener; |
| } |
| } |
| |
| public JobImpl createJob(CharSequence command) |
| { |
| synchronized (jobs) |
| { |
| int id = 1; |
| |
| boolean found; |
| do |
| { |
| found = false; |
| for (Job job : jobs) |
| { |
| if (job.id() == id) |
| { |
| found = true; |
| id++; |
| break; |
| } |
| } |
| } |
| while (found); |
| |
| JobImpl cur = currentJob(); |
| JobImpl job = new JobImpl(id, cur, command); |
| if (cur == null) |
| { |
| jobs.add(job); |
| } |
| else |
| { |
| cur.add(job); |
| } |
| return job; |
| } |
| } |
| |
| class JobImpl implements Job, Runnable |
| { |
| private final int id; |
| private final JobImpl parent; |
| private final CharSequence command; |
| private final List<Pipe> pipes = new ArrayList<>(); |
| private final List<Job> children = new ArrayList<>(); |
| private Status status = Status.Created; |
| private Future<?> future; |
| private Result result; |
| |
| public JobImpl(int id, JobImpl parent, CharSequence command) |
| { |
| this.id = id; |
| this.parent = parent; |
| this.command = command; |
| } |
| |
| void addPipe(Pipe pipe) |
| { |
| pipes.add(pipe); |
| } |
| |
| @Override |
| public int id() |
| { |
| return id; |
| } |
| |
| public CharSequence command() |
| { |
| return command; |
| } |
| |
| @Override |
| public synchronized Status status() |
| { |
| return status; |
| } |
| |
| @Override |
| public synchronized void suspend() |
| { |
| if (status == Status.Done) |
| { |
| throw new IllegalStateException("Job is finished"); |
| } |
| if (status != Status.Suspended) |
| { |
| setStatus(Status.Suspended); |
| } |
| } |
| |
| @Override |
| public synchronized void background() |
| { |
| if (status == Status.Done) |
| { |
| throw new IllegalStateException("Job is finished"); |
| } |
| if (status != Status.Background) |
| { |
| setStatus(Status.Background); |
| } |
| } |
| |
| @Override |
| public synchronized void foreground() |
| { |
| if (status == Status.Done) |
| { |
| throw new IllegalStateException("Job is finished"); |
| } |
| JobImpl cr = CommandSessionImpl.currentJob(); |
| JobImpl fg = foregroundJob(); |
| if (parent == null && fg != null && fg != this && fg != cr) |
| { |
| throw new IllegalStateException("A job is already in foreground"); |
| } |
| if (status != Status.Foreground) |
| { |
| setStatus(Status.Foreground); |
| } |
| } |
| |
| @Override |
| public void interrupt() |
| { |
| Future<?> future; |
| List<Job> children; |
| synchronized (this) |
| { |
| future = this.future; |
| } |
| synchronized (this.children) |
| { |
| children = new ArrayList<>(this.children); |
| } |
| for (Job job : children) |
| { |
| job.interrupt(); |
| } |
| if (future != null) |
| { |
| future.cancel(true); |
| } |
| } |
| |
| protected synchronized void done() |
| { |
| if (status == Status.Done) |
| { |
| throw new IllegalStateException("Job is finished"); |
| } |
| setStatus(Status.Done); |
| } |
| |
| private void setStatus(Status newStatus) |
| { |
| setStatus(newStatus, true); |
| } |
| |
| private void setStatus(Status newStatus, boolean callListeners) |
| { |
| Status previous; |
| synchronized (this) |
| { |
| previous = this.status; |
| status = newStatus; |
| } |
| if (callListeners) |
| { |
| JobListener listener; |
| synchronized (jobs) |
| { |
| listener = jobListener; |
| if (newStatus == Status.Done) |
| { |
| jobs.remove(this); |
| } |
| } |
| if (listener != null) |
| { |
| listener.jobChanged(this, previous, newStatus); |
| } |
| } |
| synchronized (this) |
| { |
| JobImpl.this.notifyAll(); |
| } |
| } |
| |
| @Override |
| public synchronized Result result() |
| { |
| return result; |
| } |
| |
| @Override |
| public Job parent() |
| { |
| return parent; |
| } |
| |
| /** |
| * Start the job. |
| * If the job is started in foreground, |
| * waits for the job to finish or to be |
| * suspended or moved to background. |
| * |
| * @param status the desired job status |
| * @return <code>null</code> if the job |
| * has been suspended or moved to background, |
| * |
| */ |
| public synchronized Result start(Status status) throws InterruptedException |
| { |
| if (status == Status.Created || status == Status.Done) |
| { |
| throw new IllegalArgumentException("Illegal start status"); |
| } |
| if (this.status != Status.Created) |
| { |
| throw new IllegalStateException("Job already started"); |
| } |
| switch (status) |
| { |
| case Suspended: |
| suspend(); |
| break; |
| case Background: |
| background(); |
| break; |
| case Foreground: |
| foreground(); |
| break; |
| case Created: |
| case Done: |
| } |
| future = executor.submit(this); |
| while (this.status == Status.Foreground) |
| { |
| JobImpl.this.wait(); |
| } |
| return result; |
| } |
| |
| public List<Process> processes() |
| { |
| return Collections.unmodifiableList((List<? extends Process>)pipes); |
| } |
| |
| @Override |
| public CommandSession session() |
| { |
| return CommandSessionImpl.this; |
| } |
| |
| public void run() |
| { |
| Thread thread = Thread.currentThread(); |
| String name = thread.getName(); |
| try |
| { |
| thread.setName("job controller " + id); |
| |
| List<Callable<Result>> wrapped = new ArrayList<Callable<Result>>(pipes); |
| List<Future<Result>> results = executor.invokeAll(wrapped); |
| |
| // Get pipe exceptions |
| Exception pipeException = null; |
| for (int i = 0; i < results.size() - 1; i++) |
| { |
| Future<Result> future = results.get(i); |
| Throwable e; |
| try |
| { |
| Result r = future.get(); |
| e = r.exception; |
| } |
| catch (ExecutionException ee) |
| { |
| e = ee.getCause(); |
| } |
| if (e != null) |
| { |
| if (pipeException == null) |
| { |
| pipeException = new Exception("Exception caught during pipe execution"); |
| } |
| pipeException.addSuppressed(e); |
| } |
| } |
| put(Closure.PIPE_EXCEPTION, pipeException); |
| |
| result = results.get(results.size() - 1).get(); |
| } |
| catch (Exception e) |
| { |
| result = new Result(e); |
| } |
| catch (Throwable t) |
| { |
| result = new Result(new ExecutionException(t)); |
| } |
| finally |
| { |
| done(); |
| thread.setName(name); |
| } |
| } |
| |
| public void add(Job child) |
| { |
| synchronized (children) |
| { |
| children.add(child); |
| } |
| } |
| } |
| |
| } |