SSHD: Add support for generic commands
Change-Id: I5a60710323ca674d70e34f7451422ec167105429
1 files deleted
14 files added
6 files modified
| | |
| | | */ |
| | | package com.gitblit.transport.ssh; |
| | | |
| | | import java.io.BufferedWriter; |
| | | import java.io.IOException; |
| | | import java.io.InputStream; |
| | | import java.io.OutputStream; |
| | | import java.io.OutputStreamWriter; |
| | | import java.io.PrintWriter; |
| | | |
| | | import org.apache.sshd.server.Command; |
| | | import org.apache.sshd.server.Environment; |
| | |
| | | import org.apache.sshd.server.SessionAware; |
| | | import org.apache.sshd.server.session.ServerSession; |
| | | |
| | | import com.google.common.base.Charsets; |
| | | |
| | | /** |
| | | * |
| | | * @author Eric Myrhe |
| | | * |
| | | */ |
| | | abstract class AbstractSshCommand implements Command, SessionAware { |
| | | public abstract class AbstractSshCommand implements Command, SessionAware { |
| | | |
| | | protected InputStream in; |
| | | |
| | |
| | | @Override |
| | | public void destroy() {} |
| | | |
| | | protected static PrintWriter toPrintWriter(final OutputStream o) { |
| | | return new PrintWriter(new BufferedWriter(new OutputStreamWriter(o, Charsets.UTF_8))); |
| | | } |
| | | |
| | | @Override |
| | | public abstract void start(Environment env) throws IOException; |
| | | } |
New file |
| | |
| | | package com.gitblit.transport.ssh; |
| | | |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | import javax.inject.Inject; |
| | | import javax.inject.Named; |
| | | import javax.inject.Provider; |
| | | |
| | | import org.apache.sshd.server.Command; |
| | | |
| | | import com.gitblit.transport.ssh.commands.DispatchCommand; |
| | | import com.google.common.collect.Maps; |
| | | import com.google.common.collect.Sets; |
| | | |
| | | public class CommandDispatcher extends DispatchCommand { |
| | | |
| | | Provider<Command> repo; |
| | | Provider<Command> version; |
| | | |
| | | @Inject |
| | | public CommandDispatcher(final @Named("create-repository") Provider<Command> repo, |
| | | final @Named("version") Provider<Command> version) { |
| | | this.repo = repo; |
| | | this.version = version; |
| | | } |
| | | |
| | | public DispatchCommand get() { |
| | | DispatchCommand root = new DispatchCommand(); |
| | | Map<String, Provider<Command>> origin = Maps.newHashMapWithExpectedSize(2); |
| | | origin.put("gitblit", new Provider<Command>() { |
| | | @Override |
| | | public Command get() { |
| | | Set<Provider<Command>> gitblit = Sets.newHashSetWithExpectedSize(2); |
| | | gitblit.add(repo); |
| | | gitblit.add(version); |
| | | Command cmd = new DispatchCommand(gitblit); |
| | | return cmd; |
| | | } |
| | | }); |
| | | root.setMap(origin); |
| | | return root; |
| | | } |
| | | } |
New file |
| | |
| | | //Copyright (C) 2013 The Android Open Source Project |
| | | // |
| | | //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.gitblit.transport.ssh; |
| | | |
| | | import static java.lang.annotation.RetentionPolicy.RUNTIME; |
| | | |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.Target; |
| | | |
| | | /** |
| | | * Annotation tagged on a concrete Command to describe what it is doing |
| | | */ |
| | | @Target({ElementType.TYPE}) |
| | | @Retention(RUNTIME) |
| | | public @interface CommandMetaData { |
| | | String name(); |
| | | String description() default ""; |
| | | } |
| | |
| | | package com.gitblit.transport.ssh; |
| | | |
| | | import java.io.IOException; |
| | | import java.util.Scanner; |
| | | import java.io.InputStream; |
| | | import java.io.OutputStream; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.concurrent.Future; |
| | | import java.util.concurrent.ScheduledExecutorService; |
| | | import java.util.concurrent.atomic.AtomicBoolean; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import javax.inject.Inject; |
| | | |
| | | import org.apache.sshd.server.Command; |
| | | import org.apache.sshd.server.CommandFactory; |
| | | import org.apache.sshd.server.Environment; |
| | | import org.apache.sshd.server.ExitCallback; |
| | | import org.apache.sshd.server.SessionAware; |
| | | import org.apache.sshd.server.session.ServerSession; |
| | | import org.eclipse.jgit.errors.RepositoryNotFoundException; |
| | | import org.eclipse.jgit.lib.Repository; |
| | | import org.eclipse.jgit.transport.PacketLineOut; |
| | |
| | | import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; |
| | | import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; |
| | | import org.eclipse.jgit.transport.resolver.UploadPackFactory; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | |
| | | import com.gitblit.git.RepositoryResolver; |
| | | import com.gitblit.transport.ssh.commands.DispatchCommand; |
| | | import com.gitblit.utils.WorkQueue; |
| | | import com.google.common.util.concurrent.Atomics; |
| | | |
| | | /** |
| | | * |
| | |
| | | * |
| | | */ |
| | | public class SshCommandFactory implements CommandFactory { |
| | | public SshCommandFactory(RepositoryResolver<SshDaemonClient> repositoryResolver, UploadPackFactory<SshDaemonClient> uploadPackFactory, ReceivePackFactory<SshDaemonClient> receivePackFactory) { |
| | | private static final Logger logger = LoggerFactory |
| | | .getLogger(SshCommandFactory.class); |
| | | private RepositoryResolver<SshSession> repositoryResolver; |
| | | |
| | | private UploadPackFactory<SshSession> uploadPackFactory; |
| | | |
| | | private ReceivePackFactory<SshSession> receivePackFactory; |
| | | private final ScheduledExecutorService startExecutor; |
| | | |
| | | private CommandDispatcher dispatcher; |
| | | |
| | | @Inject |
| | | public SshCommandFactory(RepositoryResolver<SshSession> repositoryResolver, |
| | | UploadPackFactory<SshSession> uploadPackFactory, |
| | | ReceivePackFactory<SshSession> receivePackFactory, |
| | | WorkQueue workQueue, |
| | | CommandDispatcher d) { |
| | | this.repositoryResolver = repositoryResolver; |
| | | this.uploadPackFactory = uploadPackFactory; |
| | | this.receivePackFactory = receivePackFactory; |
| | | this.dispatcher = d; |
| | | int threads = 2;//cfg.getInt("sshd","commandStartThreads", 2); |
| | | startExecutor = workQueue.createQueue(threads, "SshCommandStart"); |
| | | } |
| | | |
| | | private RepositoryResolver<SshDaemonClient> repositoryResolver; |
| | | |
| | | private UploadPackFactory<SshDaemonClient> uploadPackFactory; |
| | | |
| | | private ReceivePackFactory<SshDaemonClient> receivePackFactory; |
| | | |
| | | @Override |
| | | public Command createCommand(final String commandLine) { |
| | | Scanner commandScanner = new Scanner(commandLine); |
| | | final String command = commandScanner.next(); |
| | | final String argument = commandScanner.nextLine(); |
| | | |
| | | return new Trampoline(commandLine); |
| | | /* |
| | | if ("git-upload-pack".equals(command)) |
| | | return new UploadPackCommand(argument); |
| | | if ("git-receive-pack".equals(command)) |
| | | return new ReceivePackCommand(argument); |
| | | return new NonCommand(); |
| | | */ |
| | | } |
| | | |
| | | private class Trampoline implements Command, SessionAware { |
| | | private final String[] argv; |
| | | private InputStream in; |
| | | private OutputStream out; |
| | | private OutputStream err; |
| | | private ExitCallback exit; |
| | | private Environment env; |
| | | private DispatchCommand cmd; |
| | | private final AtomicBoolean logged; |
| | | private final AtomicReference<Future<?>> task; |
| | | |
| | | Trampoline(final String cmdLine) { |
| | | argv = split(cmdLine); |
| | | logged = new AtomicBoolean(); |
| | | task = Atomics.newReference(); |
| | | } |
| | | |
| | | @Override |
| | | public void setSession(ServerSession session) { |
| | | // TODO Auto-generated method stub |
| | | } |
| | | |
| | | public void setInputStream(final InputStream in) { |
| | | this.in = in; |
| | | } |
| | | |
| | | public void setOutputStream(final OutputStream out) { |
| | | this.out = out; |
| | | } |
| | | |
| | | public void setErrorStream(final OutputStream err) { |
| | | this.err = err; |
| | | } |
| | | |
| | | public void setExitCallback(final ExitCallback callback) { |
| | | this.exit = callback; |
| | | } |
| | | |
| | | public void start(final Environment env) throws IOException { |
| | | this.env = env; |
| | | task.set(startExecutor.submit(new Runnable() { |
| | | public void run() { |
| | | try { |
| | | onStart(); |
| | | } catch (Exception e) { |
| | | logger.warn("Cannot start command ", e); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | //return "start (user " + ctx.getSession().getUsername() + ")"; |
| | | return "start (user TODO)"; |
| | | } |
| | | })); |
| | | } |
| | | |
| | | private void onStart() throws IOException { |
| | | synchronized (this) { |
| | | //final Context old = sshScope.set(ctx); |
| | | try { |
| | | cmd = dispatcher.get(); |
| | | cmd.setArguments(argv); |
| | | cmd.setInputStream(in); |
| | | cmd.setOutputStream(out); |
| | | cmd.setErrorStream(err); |
| | | cmd.setExitCallback(new ExitCallback() { |
| | | @Override |
| | | public void onExit(int rc, String exitMessage) { |
| | | exit.onExit(translateExit(rc), exitMessage); |
| | | log(rc); |
| | | } |
| | | |
| | | @Override |
| | | public void onExit(int rc) { |
| | | exit.onExit(translateExit(rc)); |
| | | log(rc); |
| | | } |
| | | }); |
| | | cmd.start(env); |
| | | } finally { |
| | | //sshScope.set(old); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private int translateExit(final int rc) { |
| | | return rc; |
| | | // |
| | | // switch (rc) { |
| | | // case BaseCommand.STATUS_NOT_ADMIN: |
| | | // return 1; |
| | | // |
| | | // case BaseCommand.STATUS_CANCEL: |
| | | // return 15 /* SIGKILL */; |
| | | // |
| | | // case BaseCommand.STATUS_NOT_FOUND: |
| | | // return 127 /* POSIX not found */; |
| | | // |
| | | // default: |
| | | // return rc; |
| | | // } |
| | | |
| | | } |
| | | |
| | | private void log(final int rc) { |
| | | if (logged.compareAndSet(false, true)) { |
| | | //log.onExecute(cmd, rc); |
| | | logger.info("onExecute: {} exits with: {}", cmd.getClass().getSimpleName(), rc); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void destroy() { |
| | | Future<?> future = task.getAndSet(null); |
| | | if (future != null) { |
| | | future.cancel(true); |
| | | // destroyExecutor.execute(new Runnable() { |
| | | // @Override |
| | | // public void run() { |
| | | // onDestroy(); |
| | | // } |
| | | // }); |
| | | } |
| | | } |
| | | |
| | | private void onDestroy() { |
| | | synchronized (this) { |
| | | if (cmd != null) { |
| | | //final Context old = sshScope.set(ctx); |
| | | try { |
| | | cmd.destroy(); |
| | | //log(BaseCommand.STATUS_CANCEL); |
| | | } finally { |
| | | //ctx = null; |
| | | cmd = null; |
| | | //sshScope.set(old); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** Split a command line into a string array. */ |
| | | static public String[] split(String commandLine) { |
| | | final List<String> list = new ArrayList<String>(); |
| | | boolean inquote = false; |
| | | boolean inDblQuote = false; |
| | | StringBuilder r = new StringBuilder(); |
| | | for (int ip = 0; ip < commandLine.length();) { |
| | | final char b = commandLine.charAt(ip++); |
| | | switch (b) { |
| | | case '\t': |
| | | case ' ': |
| | | if (inquote || inDblQuote) |
| | | r.append(b); |
| | | else if (r.length() > 0) { |
| | | list.add(r.toString()); |
| | | r = new StringBuilder(); |
| | | } |
| | | continue; |
| | | case '\"': |
| | | if (inquote) |
| | | r.append(b); |
| | | else |
| | | inDblQuote = !inDblQuote; |
| | | continue; |
| | | case '\'': |
| | | if (inDblQuote) |
| | | r.append(b); |
| | | else |
| | | inquote = !inquote; |
| | | continue; |
| | | case '\\': |
| | | if (inquote || ip == commandLine.length()) |
| | | r.append(b); // literal within a quote |
| | | else |
| | | r.append(commandLine.charAt(ip++)); |
| | | continue; |
| | | default: |
| | | r.append(b); |
| | | continue; |
| | | } |
| | | } |
| | | if (r.length() > 0) { |
| | | list.add(r.toString()); |
| | | } |
| | | return list.toArray(new String[list.size()]); |
| | | } |
| | | |
| | | public abstract class RepositoryCommand extends AbstractSshCommand { |
| | |
| | | public void start(Environment env) throws IOException { |
| | | Repository db = null; |
| | | try { |
| | | SshDaemonClient client = session.getAttribute(SshDaemonClient.ATTR_KEY); |
| | | SshSession client = session.getAttribute(SshSession.KEY); |
| | | db = selectRepository(client, repositoryName); |
| | | if (db == null) return; |
| | | run(client, db); |
| | |
| | | } |
| | | } |
| | | |
| | | protected Repository selectRepository(SshDaemonClient client, String name) throws IOException { |
| | | protected Repository selectRepository(SshSession client, String name) throws IOException { |
| | | try { |
| | | return openRepository(client, name); |
| | | } catch (ServiceMayNotContinueException e) { |
| | |
| | | } |
| | | } |
| | | |
| | | protected Repository openRepository(SshDaemonClient client, String name) |
| | | protected Repository openRepository(SshSession client, String name) |
| | | throws ServiceMayNotContinueException { |
| | | // Assume any attempt to use \ was by a Windows client |
| | | // and correct to the more typical / used in Git URIs. |
| | |
| | | } |
| | | } |
| | | |
| | | protected abstract void run(SshDaemonClient client, Repository db) |
| | | protected abstract void run(SshSession client, Repository db) |
| | | throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException; |
| | | } |
| | | |
| | |
| | | public UploadPackCommand(String repositoryName) { super(repositoryName); } |
| | | |
| | | @Override |
| | | protected void run(SshDaemonClient client, Repository db) |
| | | protected void run(SshSession client, Repository db) |
| | | throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException { |
| | | UploadPack up = uploadPackFactory.create(client, db); |
| | | up.upload(in, out, null); |
| | |
| | | public ReceivePackCommand(String repositoryName) { super(repositoryName); } |
| | | |
| | | @Override |
| | | protected void run(SshDaemonClient client, Repository db) |
| | | protected void run(SshSession client, Repository db) |
| | | throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException { |
| | | ReceivePack rp = receivePackFactory.create(client, db); |
| | | rp.receive(in, out, null); |
| | |
| | | |
| | | import java.io.IOException; |
| | | import java.net.InetSocketAddress; |
| | | import java.net.SocketAddress; |
| | | import java.security.InvalidKeyException; |
| | | import java.util.Arrays; |
| | | import java.util.Iterator; |
| | | import java.util.LinkedList; |
| | | import java.util.List; |
| | | |
| | | import javax.inject.Inject; |
| | | |
| | | import org.apache.mina.core.future.IoFuture; |
| | | import org.apache.mina.core.future.IoFutureListener; |
| | |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | |
| | | import com.gitblit.utils.IdGenerator; |
| | | |
| | | /** |
| | | * |
| | | * @author Eric Myhre |
| | |
| | | |
| | | private static final Logger log = LoggerFactory.getLogger(SshCommandServer.class); |
| | | |
| | | public SshCommandServer() { |
| | | @Inject |
| | | public SshCommandServer(final IdGenerator idGenerator) { |
| | | setSessionFactory(new SessionFactory() { |
| | | @Override |
| | | protected ServerSession createSession(final IoSession io) throws Exception { |
| | |
| | | } |
| | | |
| | | final ServerSession s = (ServerSession) super.createSession(io); |
| | | s.setAttribute(SshDaemonClient.ATTR_KEY, new SshDaemonClient()); |
| | | SocketAddress peer = io.getRemoteAddress(); |
| | | SshSession session = new SshSession(idGenerator.next(), peer); |
| | | s.setAttribute(SshSession.KEY, session); |
| | | |
| | | io.getCloseFuture().addListener(new IoFutureListener<IoFuture>() { |
| | | @Override |
| | |
| | | import java.text.MessageFormat;
|
| | | import java.util.concurrent.atomic.AtomicBoolean;
|
| | |
|
| | | import javax.inject.Named;
|
| | | import javax.inject.Singleton;
|
| | |
|
| | | import org.apache.sshd.server.Command;
|
| | | import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider;
|
| | | import org.eclipse.jgit.internal.JGitText;
|
| | | import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
|
| | |
| | | import com.gitblit.git.GitblitUploadPackFactory;
|
| | | import com.gitblit.git.RepositoryResolver;
|
| | | import com.gitblit.manager.IGitblit;
|
| | | import com.gitblit.transport.ssh.commands.CreateRepository;
|
| | | import com.gitblit.transport.ssh.commands.VersionCommand;
|
| | | import com.gitblit.utils.IdGenerator;
|
| | | import com.gitblit.utils.StringUtils;
|
| | |
|
| | | import dagger.Module;
|
| | | import dagger.ObjectGraph;
|
| | | import dagger.Provides;
|
| | |
|
| | | /**
|
| | | * Manager for the ssh transport. Roughly analogous to the
|
| | |
| | |
|
| | | private SshCommandServer sshd;
|
| | |
|
| | | private RepositoryResolver<SshDaemonClient> repositoryResolver;
|
| | |
|
| | | private UploadPackFactory<SshDaemonClient> uploadPackFactory;
|
| | |
|
| | | private ReceivePackFactory<SshDaemonClient> receivePackFactory;
|
| | | private IGitblit gitblit;
|
| | |
|
| | | /**
|
| | | * Construct the Gitblit SSH daemon.
|
| | |
| | | */
|
| | | public SshDaemon(IGitblit gitblit) {
|
| | |
|
| | | this.gitblit = gitblit;
|
| | | IStoredSettings settings = gitblit.getSettings();
|
| | | int port = settings.getInteger(Keys.git.sshPort, 0);
|
| | | String bindInterface = settings.getString(Keys.git.sshBindInterface, "localhost");
|
| | |
| | | myAddress = new InetSocketAddress(bindInterface, port);
|
| | | }
|
| | |
|
| | | sshd = new SshCommandServer();
|
| | | ObjectGraph graph = ObjectGraph.create(new SshModule());
|
| | | sshd = graph.get(SshCommandServer.class);
|
| | | sshd.setPort(myAddress.getPort());
|
| | | sshd.setHost(myAddress.getHostName());
|
| | | sshd.setup();
|
| | |
| | | sshd.setPublickeyAuthenticator(new SshKeyAuthenticator(gitblit));
|
| | |
|
| | | run = new AtomicBoolean(false);
|
| | | repositoryResolver = new RepositoryResolver<SshDaemonClient>(gitblit);
|
| | | uploadPackFactory = new GitblitUploadPackFactory<SshDaemonClient>(gitblit);
|
| | | receivePackFactory = new GitblitReceivePackFactory<SshDaemonClient>(gitblit);
|
| | |
|
| | | sshd.setCommandFactory(new SshCommandFactory(
|
| | | repositoryResolver,
|
| | | uploadPackFactory,
|
| | | receivePackFactory
|
| | | ));
|
| | | SshCommandFactory f = graph.get(SshCommandFactory.class);
|
| | | sshd.setCommandFactory(f);
|
| | | }
|
| | |
|
| | | public int getPort() {
|
| | |
| | | }
|
| | | }
|
| | | }
|
| | |
|
| | | @Module(library = true,
|
| | | injects = {
|
| | | IGitblit.class,
|
| | | SshCommandFactory.class,
|
| | | SshCommandServer.class,
|
| | | })
|
| | | public class SshModule {
|
| | | @Provides @Named("create-repository") Command provideCreateRepository() {
|
| | | return new CreateRepository();
|
| | | }
|
| | |
|
| | | @Provides @Named("version") Command provideVersion() {
|
| | | return new VersionCommand();
|
| | | }
|
| | |
|
| | | // @Provides(type=Type.SET) @Named("git") Command provideVersionCommand2() {
|
| | | // return new CreateRepository();
|
| | | // }
|
| | |
|
| | | // @Provides @Named("git") DispatchCommand providesGitCommand() {
|
| | | // return new DispatchCommand("git");
|
| | | // }
|
| | |
|
| | | // @Provides (type=Type.SET) Provider<Command> provideNonCommand() {
|
| | | // return new SshCommandFactory.NonCommand();
|
| | | // }
|
| | |
|
| | | @Provides @Singleton IdGenerator provideIdGenerator() {
|
| | | return new IdGenerator();
|
| | | }
|
| | |
|
| | | @Provides @Singleton RepositoryResolver<SshSession> provideRepositoryResolver() {
|
| | | return new RepositoryResolver<SshSession>(provideGitblit());
|
| | | }
|
| | |
|
| | | @Provides @Singleton UploadPackFactory<SshSession> provideUploadPackFactory() {
|
| | | return new GitblitUploadPackFactory<SshSession>(provideGitblit());
|
| | | }
|
| | |
|
| | | @Provides @Singleton ReceivePackFactory<SshSession> provideReceivePackFactory() {
|
| | | return new GitblitReceivePackFactory<SshSession>(provideGitblit());
|
| | | }
|
| | |
|
| | | @Provides @Singleton IGitblit provideGitblit() {
|
| | | return SshDaemon.this.gitblit;
|
| | | }
|
| | | }
|
| | | }
|
| | |
| | | /* |
| | | * Copyright 2014 gitblit.com. |
| | | * |
| | | * 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 |
| | | * 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. |
| | | * 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.gitblit.transport.ssh; |
| | | |
| | | import java.io.File; |
| | | import java.io.IOException; |
| | | import java.security.PublicKey; |
| | | import java.util.Locale; |
| | | import java.util.concurrent.ExecutionException; |
| | | |
| | | import org.apache.commons.codec.binary.Base64; |
| | | import org.apache.sshd.common.util.Buffer; |
| | | import org.apache.sshd.server.PublickeyAuthenticator; |
| | | import org.apache.sshd.server.session.ServerSession; |
| | | import org.eclipse.jgit.lib.Constants; |
| | | |
| | | import com.gitblit.manager.IGitblit; |
| | | import com.google.common.base.Charsets; |
| | | import com.google.common.cache.CacheBuilder; |
| | | import com.google.common.cache.CacheLoader; |
| | | import com.google.common.cache.LoadingCache; |
| | | import com.google.common.cache.Weigher; |
| | | import com.google.common.io.Files; |
| | | |
| | | /** |
| | | * |
| | |
| | | |
| | | protected final IGitblit gitblit; |
| | | |
| | | LoadingCache<String, SshKeyCacheEntry> sshKeyCache = CacheBuilder |
| | | .newBuilder().maximumWeight(2 << 20).weigher(new SshKeyCacheWeigher()) |
| | | .build(new CacheLoader<String, SshKeyCacheEntry>() { |
| | | public SshKeyCacheEntry load(String key) throws Exception { |
| | | return loadKey(key); |
| | | } |
| | | |
| | | private SshKeyCacheEntry loadKey(String key) { |
| | | try { |
| | | // TODO(davido): retrieve absolute path to public key directory: |
| | | //String dir = gitblit.getSettings().getString("public_key_dir", "data/ssh"); |
| | | String dir = "/tmp/"; |
| | | // Expect public key file name in form: <username.pub> in |
| | | File file = new File(dir + key + ".pub"); |
| | | String str = Files.toString(file, Charsets.ISO_8859_1); |
| | | final String[] parts = str.split(" "); |
| | | final byte[] bin = |
| | | Base64.decodeBase64(Constants.encodeASCII(parts[1])); |
| | | return new SshKeyCacheEntry(key, new Buffer(bin).getRawPublicKey()); |
| | | } catch (IOException e) { |
| | | throw new RuntimeException("Canot read public key", e); |
| | | } |
| | | } |
| | | }); |
| | | |
| | | public SshKeyAuthenticator(IGitblit gitblit) { |
| | | this.gitblit = gitblit; |
| | | } |
| | | |
| | | @Override |
| | | public boolean authenticate(String username, PublicKey key, ServerSession session) { |
| | | // TODO actually authenticate |
| | | public boolean authenticate(String username, final PublicKey suppliedKey, |
| | | final ServerSession session) { |
| | | final SshSession sd = session.getAttribute(SshSession.KEY); |
| | | |
| | | // if (config.getBoolean("auth", "userNameToLowerCase", false)) { |
| | | username = username.toLowerCase(Locale.US); |
| | | // } |
| | | try { |
| | | // TODO: allow multiple public keys per user |
| | | SshKeyCacheEntry key = sshKeyCache.get(username); |
| | | if (key == null) { |
| | | sd.authenticationError(username, "no-matching-key"); |
| | | return false; |
| | | } |
| | | |
| | | if (key.match(suppliedKey)) { |
| | | return success(username, session, sd); |
| | | } |
| | | return false; |
| | | } catch (ExecutionException e) { |
| | | sd.authenticationError(username, "user-not-found"); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | boolean success(String username, ServerSession session, SshSession sd) { |
| | | sd.authenticationSuccess(username); |
| | | /* |
| | | * sshLog.onLogin(); |
| | | * |
| | | * GerritServerSession s = (GerritServerSession) session; |
| | | * s.addCloseSessionListener( new SshFutureListener<CloseFuture>() { |
| | | * |
| | | * @Override public void operationComplete(CloseFuture future) { final |
| | | * Context ctx = sshScope.newContext(null, sd, null); final Context old = |
| | | * sshScope.set(ctx); try { sshLog.onLogout(); } finally { |
| | | * sshScope.set(old); } } }); } |
| | | */ |
| | | return true; |
| | | } |
| | | |
| | | private static class SshKeyCacheWeigher implements |
| | | Weigher<String, SshKeyCacheEntry> { |
| | | @Override |
| | | public int weigh(String key, SshKeyCacheEntry value) { |
| | | return key.length() + value.weigh(); |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | |
| | | package com.gitblit.transport.ssh; |
| | | |
| | | import java.security.PublicKey; |
| | | |
| | | class SshKeyCacheEntry { |
| | | private final String user; |
| | | private final PublicKey publicKey; |
| | | |
| | | SshKeyCacheEntry(String user, PublicKey publicKey) { |
| | | this.user = user; |
| | | this.publicKey = publicKey; |
| | | } |
| | | |
| | | String getUser() { |
| | | return user; |
| | | } |
| | | |
| | | boolean match(PublicKey inkey) { |
| | | return publicKey.equals(inkey); |
| | | } |
| | | |
| | | int weigh() { |
| | | return publicKey.getEncoded().length; |
| | | } |
| | | } |
New file |
| | |
| | | /* |
| | | * Copyright 2014 gitblit.com. |
| | | * |
| | | * 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.gitblit.transport.ssh; |
| | | |
| | | import java.net.InetAddress; |
| | | import java.net.InetSocketAddress; |
| | | import java.net.SocketAddress; |
| | | |
| | | import org.apache.sshd.common.Session.AttributeKey; |
| | | |
| | | /** |
| | | * |
| | | * @author Eric Myrhe |
| | | * |
| | | */ |
| | | public class SshSession { |
| | | public static final AttributeKey<SshSession> KEY = |
| | | new AttributeKey<SshSession>(); |
| | | |
| | | private final int sessionId; |
| | | private final SocketAddress remoteAddress; |
| | | private final String remoteAsString; |
| | | |
| | | private volatile String username; |
| | | private volatile String authError; |
| | | |
| | | SshSession(int sessionId, SocketAddress peer) { |
| | | this.sessionId = sessionId; |
| | | this.remoteAddress = peer; |
| | | this.remoteAsString = format(remoteAddress); |
| | | } |
| | | |
| | | public SocketAddress getRemoteAddress() { |
| | | return remoteAddress; |
| | | } |
| | | |
| | | String getRemoteAddressAsString() { |
| | | return remoteAsString; |
| | | } |
| | | |
| | | public String getRemoteUser() { |
| | | return username; |
| | | } |
| | | |
| | | /** Unique session number, assigned during connect. */ |
| | | public int getSessionId() { |
| | | return sessionId; |
| | | } |
| | | |
| | | String getUsername() { |
| | | return username; |
| | | } |
| | | |
| | | String getAuthenticationError() { |
| | | return authError; |
| | | } |
| | | |
| | | void authenticationSuccess(String user) { |
| | | username = user; |
| | | authError = null; |
| | | } |
| | | |
| | | void authenticationError(String user, String error) { |
| | | username = user; |
| | | authError = error; |
| | | } |
| | | |
| | | /** @return {@code true} if the authentication did not succeed. */ |
| | | boolean isAuthenticationError() { |
| | | return authError != null; |
| | | } |
| | | |
| | | private static String format(final SocketAddress remote) { |
| | | if (remote instanceof InetSocketAddress) { |
| | | final InetSocketAddress sa = (InetSocketAddress) remote; |
| | | |
| | | final InetAddress in = sa.getAddress(); |
| | | if (in != null) { |
| | | return in.getHostAddress(); |
| | | } |
| | | |
| | | final String hostName = sa.getHostName(); |
| | | if (hostName != null) { |
| | | return hostName; |
| | | } |
| | | } |
| | | return remote.toString(); |
| | | } |
| | | } |
New file |
| | |
| | | // Copyright (C) 2009 The Android Open Source Project |
| | | // |
| | | // 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.gitblit.transport.ssh.commands; |
| | | |
| | | import java.io.IOException; |
| | | import java.io.InputStream; |
| | | import java.io.InterruptedIOException; |
| | | import java.io.OutputStream; |
| | | import java.io.StringWriter; |
| | | import java.util.concurrent.Future; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import org.apache.sshd.common.SshException; |
| | | import org.apache.sshd.server.Command; |
| | | import org.apache.sshd.server.Environment; |
| | | import org.apache.sshd.server.ExitCallback; |
| | | import org.kohsuke.args4j.Argument; |
| | | import org.kohsuke.args4j.CmdLineException; |
| | | import org.kohsuke.args4j.Option; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | |
| | | import com.gitblit.transport.ssh.AbstractSshCommand; |
| | | import com.gitblit.utils.IdGenerator; |
| | | import com.gitblit.utils.WorkQueue; |
| | | import com.gitblit.utils.cli.CmdLineParser; |
| | | import com.google.common.base.Charsets; |
| | | import com.google.common.util.concurrent.Atomics; |
| | | |
| | | public abstract class BaseCommand extends AbstractSshCommand { |
| | | private static final Logger log = LoggerFactory |
| | | .getLogger(BaseCommand.class); |
| | | |
| | | /** Text of the command line which lead up to invoking this instance. */ |
| | | private String commandName = ""; |
| | | |
| | | /** Unparsed command line options. */ |
| | | private String[] argv; |
| | | |
| | | /** The task, as scheduled on a worker thread. */ |
| | | private final AtomicReference<Future<?>> task; |
| | | |
| | | private final WorkQueue.Executor executor; |
| | | |
| | | public BaseCommand() { |
| | | task = Atomics.newReference(); |
| | | IdGenerator gen = new IdGenerator(); |
| | | WorkQueue w = new WorkQueue(gen); |
| | | this.executor = w.getDefaultQueue(); |
| | | } |
| | | |
| | | public void setInputStream(final InputStream in) { |
| | | this.in = in; |
| | | } |
| | | |
| | | public void setOutputStream(final OutputStream out) { |
| | | this.out = out; |
| | | } |
| | | |
| | | public void setErrorStream(final OutputStream err) { |
| | | this.err = err; |
| | | } |
| | | |
| | | public void setExitCallback(final ExitCallback callback) { |
| | | this.exit = callback; |
| | | } |
| | | |
| | | protected void provideStateTo(final Command cmd) { |
| | | cmd.setInputStream(in); |
| | | cmd.setOutputStream(out); |
| | | cmd.setErrorStream(err); |
| | | cmd.setExitCallback(exit); |
| | | } |
| | | |
| | | protected String getName() { |
| | | return commandName; |
| | | } |
| | | |
| | | void setName(final String prefix) { |
| | | this.commandName = prefix; |
| | | } |
| | | |
| | | public String[] getArguments() { |
| | | return argv; |
| | | } |
| | | |
| | | public void setArguments(final String[] argv) { |
| | | this.argv = argv; |
| | | } |
| | | |
| | | /** |
| | | * Parses the command line argument, injecting parsed values into fields. |
| | | * <p> |
| | | * This method must be explicitly invoked to cause a parse. |
| | | * |
| | | * @throws UnloggedFailure if the command line arguments were invalid. |
| | | * @see Option |
| | | * @see Argument |
| | | */ |
| | | protected void parseCommandLine() throws UnloggedFailure { |
| | | parseCommandLine(this); |
| | | } |
| | | |
| | | /** |
| | | * Parses the command line argument, injecting parsed values into fields. |
| | | * <p> |
| | | * This method must be explicitly invoked to cause a parse. |
| | | * |
| | | * @param options object whose fields declare Option and Argument annotations |
| | | * to describe the parameters of the command. Usually {@code this}. |
| | | * @throws UnloggedFailure if the command line arguments were invalid. |
| | | * @see Option |
| | | * @see Argument |
| | | */ |
| | | protected void parseCommandLine(Object options) throws UnloggedFailure { |
| | | final CmdLineParser clp = newCmdLineParser(options); |
| | | try { |
| | | clp.parseArgument(argv); |
| | | } catch (IllegalArgumentException err) { |
| | | if (!clp.wasHelpRequestedByOption()) { |
| | | throw new UnloggedFailure(1, "fatal: " + err.getMessage()); |
| | | } |
| | | } catch (CmdLineException err) { |
| | | if (!clp.wasHelpRequestedByOption()) { |
| | | throw new UnloggedFailure(1, "fatal: " + err.getMessage()); |
| | | } |
| | | } |
| | | |
| | | if (clp.wasHelpRequestedByOption()) { |
| | | StringWriter msg = new StringWriter(); |
| | | clp.printDetailedUsage(commandName, msg); |
| | | msg.write(usage()); |
| | | throw new UnloggedFailure(1, msg.toString()); |
| | | } |
| | | } |
| | | |
| | | /** Construct a new parser for this command's received command line. */ |
| | | protected CmdLineParser newCmdLineParser(Object options) { |
| | | return new CmdLineParser(options); |
| | | } |
| | | |
| | | protected String usage() { |
| | | return ""; |
| | | } |
| | | |
| | | private final class TaskThunk implements com.gitblit.utils.WorkQueue.CancelableRunnable { |
| | | private final CommandRunnable thunk; |
| | | private final String taskName; |
| | | |
| | | private TaskThunk(final CommandRunnable thunk) { |
| | | this.thunk = thunk; |
| | | |
| | | // TODO |
| | | // StringBuilder m = new StringBuilder("foo"); |
| | | // m.append(context.getCommandLine()); |
| | | // if (userProvider.get().isIdentifiedUser()) { |
| | | // IdentifiedUser u = (IdentifiedUser) userProvider.get(); |
| | | // m.append(" (").append(u.getAccount().getUserName()).append(")"); |
| | | // } |
| | | this.taskName = "foo";//m.toString(); |
| | | } |
| | | |
| | | @Override |
| | | public void cancel() { |
| | | synchronized (this) { |
| | | //final Context old = sshScope.set(context); |
| | | try { |
| | | //onExit(/*STATUS_CANCEL*/); |
| | | } finally { |
| | | //sshScope.set(old); |
| | | } |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void run() { |
| | | synchronized (this) { |
| | | final Thread thisThread = Thread.currentThread(); |
| | | final String thisName = thisThread.getName(); |
| | | int rc = 0; |
| | | //final Context old = sshScope.set(context); |
| | | try { |
| | | //context.started = TimeUtil.nowMs(); |
| | | thisThread.setName("SSH " + taskName); |
| | | |
| | | thunk.run(); |
| | | |
| | | out.flush(); |
| | | err.flush(); |
| | | } catch (Throwable e) { |
| | | try { |
| | | out.flush(); |
| | | } catch (Throwable e2) { |
| | | } |
| | | try { |
| | | err.flush(); |
| | | } catch (Throwable e2) { |
| | | } |
| | | rc = handleError(e); |
| | | } finally { |
| | | try { |
| | | onExit(rc); |
| | | } finally { |
| | | thisThread.setName(thisName); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return taskName; |
| | | } |
| | | } |
| | | |
| | | /** Runnable function which can throw an exception. */ |
| | | public static interface CommandRunnable { |
| | | public void run() throws Exception; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Spawn a function into its own thread. |
| | | * <p> |
| | | * Typically this should be invoked within {@link Command#start(Environment)}, |
| | | * such as: |
| | | * |
| | | * <pre> |
| | | * startThread(new Runnable() { |
| | | * public void run() { |
| | | * runImp(); |
| | | * } |
| | | * }); |
| | | * </pre> |
| | | * |
| | | * @param thunk the runnable to execute on the thread, performing the |
| | | * command's logic. |
| | | */ |
| | | protected void startThread(final Runnable thunk) { |
| | | startThread(new CommandRunnable() { |
| | | @Override |
| | | public void run() throws Exception { |
| | | thunk.run(); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * Terminate this command and return a result code to the remote client. |
| | | * <p> |
| | | * Commands should invoke this at most once. Once invoked, the command may |
| | | * lose access to request based resources as any callbacks previously |
| | | * registered with {@link RequestCleanup} will fire. |
| | | * |
| | | * @param rc exit code for the remote client. |
| | | */ |
| | | protected void onExit(final int rc) { |
| | | exit.onExit(rc); |
| | | // if (cleanup != null) { |
| | | // cleanup.run(); |
| | | // } |
| | | } |
| | | |
| | | private int handleError(final Throwable e) { |
| | | if ((e.getClass() == IOException.class |
| | | && "Pipe closed".equals(e.getMessage())) |
| | | || // |
| | | (e.getClass() == SshException.class |
| | | && "Already closed".equals(e.getMessage())) |
| | | || // |
| | | e.getClass() == InterruptedIOException.class) { |
| | | // This is sshd telling us the client just dropped off while |
| | | // we were waiting for a read or a write to complete. Either |
| | | // way its not really a fatal error. Don't log it. |
| | | // |
| | | return 127; |
| | | } |
| | | |
| | | if (e instanceof UnloggedFailure) { |
| | | } else { |
| | | final StringBuilder m = new StringBuilder(); |
| | | m.append("Internal server error"); |
| | | // if (userProvider.get().isIdentifiedUser()) { |
| | | // final IdentifiedUser u = (IdentifiedUser) userProvider.get(); |
| | | // m.append(" (user "); |
| | | // m.append(u.getAccount().getUserName()); |
| | | // m.append(" account "); |
| | | // m.append(u.getAccountId()); |
| | | // m.append(")"); |
| | | // } |
| | | // m.append(" during "); |
| | | // m.append(contextProvider.get().getCommandLine()); |
| | | log.error(m.toString(), e); |
| | | } |
| | | |
| | | if (e instanceof Failure) { |
| | | final Failure f = (Failure) e; |
| | | try { |
| | | err.write((f.getMessage() + "\n").getBytes(Charsets.UTF_8)); |
| | | err.flush(); |
| | | } catch (IOException e2) { |
| | | } catch (Throwable e2) { |
| | | log.warn("Cannot send failure message to client", e2); |
| | | } |
| | | return f.exitCode; |
| | | |
| | | } else { |
| | | try { |
| | | err.write("fatal: internal server error\n".getBytes(Charsets.UTF_8)); |
| | | err.flush(); |
| | | } catch (IOException e2) { |
| | | } catch (Throwable e2) { |
| | | log.warn("Cannot send internal server error message to client", e2); |
| | | } |
| | | return 128; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Spawn a function into its own thread. |
| | | * <p> |
| | | * Typically this should be invoked within {@link Command#start(Environment)}, |
| | | * such as: |
| | | * |
| | | * <pre> |
| | | * startThread(new CommandRunnable() { |
| | | * public void run() throws Exception { |
| | | * runImp(); |
| | | * } |
| | | * }); |
| | | * </pre> |
| | | * <p> |
| | | * If the function throws an exception, it is translated to a simple message |
| | | * for the client, a non-zero exit code, and the stack trace is logged. |
| | | * |
| | | * @param thunk the runnable to execute on the thread, performing the |
| | | * command's logic. |
| | | */ |
| | | protected void startThread(final CommandRunnable thunk) { |
| | | final TaskThunk tt = new TaskThunk(thunk); |
| | | task.set(executor.submit(tt)); |
| | | } |
| | | |
| | | /** Thrown from {@link CommandRunnable#run()} with client message and code. */ |
| | | public static class Failure extends Exception { |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | final int exitCode; |
| | | |
| | | /** |
| | | * Create a new failure. |
| | | * |
| | | * @param exitCode exit code to return the client, which indicates the |
| | | * failure status of this command. Should be between 1 and 255, |
| | | * inclusive. |
| | | * @param msg message to also send to the client's stderr. |
| | | */ |
| | | public Failure(final int exitCode, final String msg) { |
| | | this(exitCode, msg, null); |
| | | } |
| | | |
| | | /** |
| | | * Create a new failure. |
| | | * |
| | | * @param exitCode exit code to return the client, which indicates the |
| | | * failure status of this command. Should be between 1 and 255, |
| | | * inclusive. |
| | | * @param msg message to also send to the client's stderr. |
| | | * @param why stack trace to include in the server's log, but is not sent to |
| | | * the client's stderr. |
| | | */ |
| | | public Failure(final int exitCode, final String msg, final Throwable why) { |
| | | super(msg, why); |
| | | this.exitCode = exitCode; |
| | | } |
| | | } |
| | | |
| | | /** Thrown from {@link CommandRunnable#run()} with client message and code. */ |
| | | public static class UnloggedFailure extends Failure { |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | /** |
| | | * Create a new failure. |
| | | * |
| | | * @param msg message to also send to the client's stderr. |
| | | */ |
| | | public UnloggedFailure(final String msg) { |
| | | this(1, msg); |
| | | } |
| | | |
| | | /** |
| | | * Create a new failure. |
| | | * |
| | | * @param exitCode exit code to return the client, which indicates the |
| | | * failure status of this command. Should be between 1 and 255, |
| | | * inclusive. |
| | | * @param msg message to also send to the client's stderr. |
| | | */ |
| | | public UnloggedFailure(final int exitCode, final String msg) { |
| | | this(exitCode, msg, null); |
| | | } |
| | | |
| | | /** |
| | | * Create a new failure. |
| | | * |
| | | * @param exitCode exit code to return the client, which indicates the |
| | | * failure status of this command. Should be between 1 and 255, |
| | | * inclusive. |
| | | * @param msg message to also send to the client's stderr. |
| | | * @param why stack trace to include in the server's log, but is not sent to |
| | | * the client's stderr. |
| | | */ |
| | | public UnloggedFailure(final int exitCode, final String msg, |
| | | final Throwable why) { |
| | | super(exitCode, msg, why); |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | /* |
| | | * Copyright 2014 gitblit.com. |
| | | * |
| | | * 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.gitblit.transport.ssh.commands; |
| | | |
| | | import org.kohsuke.args4j.Option; |
| | | |
| | | import com.gitblit.transport.ssh.CommandMetaData; |
| | | |
| | | @CommandMetaData(name = "create-repository", description = "Create new GIT repository") |
| | | public class CreateRepository extends SshCommand { |
| | | |
| | | @Option(name = "--name", aliases = {"-n"}, required = true, metaVar = "NAME", usage = "name of repository to be created") |
| | | private String name; |
| | | |
| | | @Option(name = "--description", aliases = {"-d"}, metaVar = "DESCRIPTION", usage = "description of repository") |
| | | private String repositoryDescription; |
| | | |
| | | @Override |
| | | public void run() { |
| | | stdout.println(String.format("Repository <%s> was created", name)); |
| | | } |
| | | } |
New file |
| | |
| | | // Copyright (C) 2009 The Android Open Source Project |
| | | // |
| | | // 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.gitblit.transport.ssh.commands; |
| | | |
| | | import java.io.IOException; |
| | | import java.io.StringWriter; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | import javax.inject.Provider; |
| | | |
| | | import org.apache.sshd.server.Command; |
| | | import org.apache.sshd.server.Environment; |
| | | import org.kohsuke.args4j.Argument; |
| | | |
| | | import com.gitblit.transport.ssh.CommandMetaData; |
| | | import com.gitblit.utils.cli.SubcommandHandler; |
| | | import com.google.common.base.Charsets; |
| | | import com.google.common.base.Strings; |
| | | import com.google.common.collect.Maps; |
| | | import com.google.common.collect.Sets; |
| | | |
| | | public class DispatchCommand extends BaseCommand { |
| | | |
| | | @Argument(index = 0, required = false, metaVar = "COMMAND", handler = SubcommandHandler.class) |
| | | private String commandName; |
| | | |
| | | @Argument(index = 1, multiValued = true, metaVar = "ARG") |
| | | private List<String> args = new ArrayList<String>(); |
| | | |
| | | private Set<Provider<Command>> commands; |
| | | private Map<String, Provider<Command>> map; |
| | | |
| | | public DispatchCommand() {} |
| | | |
| | | public DispatchCommand(Map<String, Provider<Command>> map) { |
| | | this.map = map; |
| | | } |
| | | |
| | | public void setMap(Map<String, Provider<Command>> m) { |
| | | map = m; |
| | | } |
| | | |
| | | public DispatchCommand(Set<Provider<Command>> commands) { |
| | | this.commands = commands; |
| | | } |
| | | |
| | | private Map<String, Provider<Command>> getMap() { |
| | | if (map == null) { |
| | | map = Maps.newHashMapWithExpectedSize(commands.size()); |
| | | for (Provider<Command> cmd : commands) { |
| | | CommandMetaData meta = cmd.get().getClass().getAnnotation(CommandMetaData.class); |
| | | map.put(meta.name(), cmd); |
| | | } |
| | | } |
| | | return map; |
| | | } |
| | | |
| | | @Override |
| | | public void start(Environment env) throws IOException { |
| | | try { |
| | | parseCommandLine(); |
| | | if (Strings.isNullOrEmpty(commandName)) { |
| | | StringWriter msg = new StringWriter(); |
| | | msg.write(usage()); |
| | | throw new UnloggedFailure(1, msg.toString()); |
| | | } |
| | | |
| | | final Provider<Command> p = getMap().get(commandName); |
| | | if (p == null) { |
| | | String msg = |
| | | (getName().isEmpty() ? "Gitblit" : getName()) + ": " |
| | | + commandName + ": not found"; |
| | | throw new UnloggedFailure(1, msg); |
| | | } |
| | | |
| | | final Command cmd = p.get(); |
| | | if (cmd instanceof BaseCommand) { |
| | | BaseCommand bc = (BaseCommand) cmd; |
| | | if (getName().isEmpty()) { |
| | | bc.setName(commandName); |
| | | } else { |
| | | bc.setName(getName() + " " + commandName); |
| | | } |
| | | bc.setArguments(args.toArray(new String[args.size()])); |
| | | } else if (!args.isEmpty()) { |
| | | throw new UnloggedFailure(1, commandName + " does not take arguments"); |
| | | } |
| | | |
| | | provideStateTo(cmd); |
| | | //atomicCmd.set(cmd); |
| | | cmd.start(env); |
| | | |
| | | } catch (UnloggedFailure e) { |
| | | String msg = e.getMessage(); |
| | | if (!msg.endsWith("\n")) { |
| | | msg += "\n"; |
| | | } |
| | | err.write(msg.getBytes(Charsets.UTF_8)); |
| | | err.flush(); |
| | | exit.onExit(e.exitCode); |
| | | } |
| | | } |
| | | |
| | | protected String usage() { |
| | | final StringBuilder usage = new StringBuilder(); |
| | | usage.append("Available commands"); |
| | | if (!getName().isEmpty()) { |
| | | usage.append(" of "); |
| | | usage.append(getName()); |
| | | } |
| | | usage.append(" are:\n"); |
| | | usage.append("\n"); |
| | | |
| | | int maxLength = -1; |
| | | Map<String, Provider<Command>> m = getMap(); |
| | | for (String name : m.keySet()) { |
| | | maxLength = Math.max(maxLength, name.length()); |
| | | } |
| | | String format = "%-" + maxLength + "s %s"; |
| | | for (String name : Sets.newTreeSet(m.keySet())) { |
| | | final Provider<Command> p = m.get(name); |
| | | usage.append(" "); |
| | | CommandMetaData meta = p.get().getClass().getAnnotation(CommandMetaData.class); |
| | | if (meta != null) { |
| | | usage.append(String.format(format, name, |
| | | Strings.nullToEmpty(meta.description()))); |
| | | } |
| | | usage.append("\n"); |
| | | } |
| | | usage.append("\n"); |
| | | |
| | | usage.append("See '"); |
| | | if (getName().indexOf(' ') < 0) { |
| | | usage.append(getName()); |
| | | usage.append(' '); |
| | | } |
| | | usage.append("COMMAND --help' for more information.\n"); |
| | | usage.append("\n"); |
| | | return usage.toString(); |
| | | } |
| | | } |
New file |
| | |
| | | // Copyright (C) 2012 The Android Open Source Project |
| | | // |
| | | // 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.gitblit.transport.ssh.commands; |
| | | |
| | | import java.io.IOException; |
| | | import java.io.PrintWriter; |
| | | |
| | | import org.apache.sshd.server.Environment; |
| | | |
| | | public abstract class SshCommand extends BaseCommand { |
| | | protected PrintWriter stdout; |
| | | protected PrintWriter stderr; |
| | | |
| | | @Override |
| | | public void start(Environment env) throws IOException { |
| | | startThread(new CommandRunnable() { |
| | | @Override |
| | | public void run() throws Exception { |
| | | parseCommandLine(); |
| | | stdout = toPrintWriter(out); |
| | | stderr = toPrintWriter(err); |
| | | try { |
| | | SshCommand.this.run(); |
| | | } finally { |
| | | stdout.flush(); |
| | | stderr.flush(); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | protected abstract void run() throws UnloggedFailure, Failure, Exception; |
| | | } |
New file |
| | |
| | | /* |
| | | * Copyright 2014 gitblit.com. |
| | | * |
| | | * 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.gitblit.transport.ssh.commands; |
| | | |
| | | import org.kohsuke.args4j.Option; |
| | | |
| | | import com.gitblit.Constants; |
| | | import com.gitblit.transport.ssh.CommandMetaData; |
| | | |
| | | @CommandMetaData(name="version", description = "Print Gitblit version") |
| | | public class VersionCommand extends SshCommand { |
| | | |
| | | @Option(name = "--verbose", aliases = {"-v"}, metaVar = "VERBOSE", usage = "Print verbose versions") |
| | | private boolean verbose; |
| | | |
| | | @Override |
| | | public void run() { |
| | | stdout.println(String.format("Version: %s", Constants.getGitBlitVersion(), |
| | | verbose)); |
| | | } |
| | | } |
New file |
| | |
| | | // Copyright (C) 2009 The Android Open Source Project |
| | | // |
| | | // 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.gitblit.utils; |
| | | |
| | | import java.util.Random; |
| | | import java.util.concurrent.atomic.AtomicInteger; |
| | | |
| | | import javax.inject.Inject; |
| | | |
| | | /** Simple class to produce 4 billion keys randomly distributed. */ |
| | | public class IdGenerator { |
| | | /** Format an id created by this class as a hex string. */ |
| | | public static String format(int id) { |
| | | final char[] r = new char[8]; |
| | | for (int p = 7; 0 <= p; p--) { |
| | | final int h = id & 0xf; |
| | | r[p] = h < 10 ? (char) ('0' + h) : (char) ('a' + (h - 10)); |
| | | id >>= 4; |
| | | } |
| | | return new String(r); |
| | | } |
| | | |
| | | private final AtomicInteger gen; |
| | | |
| | | @Inject |
| | | public IdGenerator() { |
| | | gen = new AtomicInteger(new Random().nextInt()); |
| | | } |
| | | |
| | | /** Produce the next identifier. */ |
| | | public int next() { |
| | | return mix(gen.getAndIncrement()); |
| | | } |
| | | |
| | | private static final int salt = 0x9e3779b9; |
| | | |
| | | static int mix(int in) { |
| | | return mix(salt, in); |
| | | } |
| | | |
| | | /** A very simple bit permutation to mask a simple incrementer. */ |
| | | public static int mix(final int salt, final int in) { |
| | | short v0 = hi16(in); |
| | | short v1 = lo16(in); |
| | | v0 += ((v1 << 2) + 0 ^ v1) + (salt ^ (v1 >>> 3)) + 1; |
| | | v1 += ((v0 << 2) + 2 ^ v0) + (salt ^ (v0 >>> 3)) + 3; |
| | | return result(v0, v1); |
| | | } |
| | | |
| | | /* For testing only. */ |
| | | static int unmix(final int in) { |
| | | short v0 = hi16(in); |
| | | short v1 = lo16(in); |
| | | v1 -= ((v0 << 2) + 2 ^ v0) + (salt ^ (v0 >>> 3)) + 3; |
| | | v0 -= ((v1 << 2) + 0 ^ v1) + (salt ^ (v1 >>> 3)) + 1; |
| | | return result(v0, v1); |
| | | } |
| | | |
| | | private static short hi16(final int in) { |
| | | return (short) ( // |
| | | ((in >>> 24 & 0xff)) | // |
| | | ((in >>> 16 & 0xff) << 8) // |
| | | ); |
| | | } |
| | | |
| | | private static short lo16(final int in) { |
| | | return (short) ( // |
| | | ((in >>> 8 & 0xff)) | // |
| | | ((in & 0xff) << 8) // |
| | | ); |
| | | } |
| | | |
| | | private static int result(final short v0, final short v1) { |
| | | return ((v0 & 0xff) << 24) | // |
| | | (((v0 >>> 8) & 0xff) << 16) | // |
| | | ((v1 & 0xff) << 8) | // |
| | | ((v1 >>> 8) & 0xff); |
| | | } |
| | | } |
New file |
| | |
| | | // Copyright (C) 2013 The Android Open Source Project |
| | | // |
| | | // 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.gitblit.utils; |
| | | |
| | | public interface TaskInfoFactory<T> { |
| | | T getTaskInfo(WorkQueue.Task<?> task); |
| | | } |
New file |
| | |
| | | // Copyright (C) 2009 The Android Open Source Project |
| | | // |
| | | // 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.gitblit.utils; |
| | | |
| | | import com.google.common.collect.Lists; |
| | | |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | |
| | | import java.lang.Thread.UncaughtExceptionHandler; |
| | | import java.util.ArrayList; |
| | | import java.util.Collection; |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | import java.util.concurrent.Callable; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.concurrent.CopyOnWriteArrayList; |
| | | import java.util.concurrent.Delayed; |
| | | import java.util.concurrent.ExecutionException; |
| | | import java.util.concurrent.Executors; |
| | | import java.util.concurrent.RunnableScheduledFuture; |
| | | import java.util.concurrent.ScheduledThreadPoolExecutor; |
| | | import java.util.concurrent.ThreadFactory; |
| | | import java.util.concurrent.TimeUnit; |
| | | import java.util.concurrent.TimeoutException; |
| | | import java.util.concurrent.atomic.AtomicBoolean; |
| | | import java.util.concurrent.atomic.AtomicInteger; |
| | | |
| | | import javax.inject.Inject; |
| | | |
| | | /** Delayed execution of tasks using a background thread pool. */ |
| | | public class WorkQueue { |
| | | private static final Logger log = LoggerFactory.getLogger(WorkQueue.class); |
| | | private static final UncaughtExceptionHandler LOG_UNCAUGHT_EXCEPTION = |
| | | new UncaughtExceptionHandler() { |
| | | @Override |
| | | public void uncaughtException(Thread t, Throwable e) { |
| | | log.error("WorkQueue thread " + t.getName() + " threw exception", e); |
| | | } |
| | | }; |
| | | |
| | | private Executor defaultQueue; |
| | | private final IdGenerator idGenerator; |
| | | private final CopyOnWriteArrayList<Executor> queues; |
| | | |
| | | @Inject |
| | | public WorkQueue(final IdGenerator idGenerator) { |
| | | this.idGenerator = idGenerator; |
| | | this.queues = new CopyOnWriteArrayList<Executor>(); |
| | | } |
| | | |
| | | /** Get the default work queue, for miscellaneous tasks. */ |
| | | public synchronized Executor getDefaultQueue() { |
| | | if (defaultQueue == null) { |
| | | defaultQueue = createQueue(1, "WorkQueue"); |
| | | } |
| | | return defaultQueue; |
| | | } |
| | | |
| | | /** Create a new executor queue with one thread. */ |
| | | public Executor createQueue(final int poolsize, final String prefix) { |
| | | final Executor r = new Executor(poolsize, prefix); |
| | | r.setContinueExistingPeriodicTasksAfterShutdownPolicy(false); |
| | | r.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); |
| | | queues.add(r); |
| | | return r; |
| | | } |
| | | |
| | | /** Get all of the tasks currently scheduled in any work queue. */ |
| | | public List<Task<?>> getTasks() { |
| | | final List<Task<?>> r = new ArrayList<Task<?>>(); |
| | | for (final Executor e : queues) { |
| | | e.addAllTo(r); |
| | | } |
| | | return r; |
| | | } |
| | | |
| | | public <T> List<T> getTaskInfos(TaskInfoFactory<T> factory) { |
| | | List<T> taskInfos = Lists.newArrayList(); |
| | | for (Executor exe : queues) { |
| | | for (Task<?> task : exe.getTasks()) { |
| | | taskInfos.add(factory.getTaskInfo(task)); |
| | | } |
| | | } |
| | | return taskInfos; |
| | | } |
| | | |
| | | /** Locate a task by its unique id, null if no task matches. */ |
| | | public Task<?> getTask(final int id) { |
| | | Task<?> result = null; |
| | | for (final Executor e : queues) { |
| | | final Task<?> t = e.getTask(id); |
| | | if (t != null) { |
| | | if (result != null) { |
| | | // Don't return the task if we have a duplicate. Lie instead. |
| | | return null; |
| | | } else { |
| | | result = t; |
| | | } |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | public void stop() { |
| | | for (final Executor p : queues) { |
| | | p.shutdown(); |
| | | boolean isTerminated; |
| | | do { |
| | | try { |
| | | isTerminated = p.awaitTermination(10, TimeUnit.SECONDS); |
| | | } catch (InterruptedException ie) { |
| | | isTerminated = false; |
| | | } |
| | | } while (!isTerminated); |
| | | } |
| | | queues.clear(); |
| | | } |
| | | |
| | | /** An isolated queue. */ |
| | | public class Executor extends ScheduledThreadPoolExecutor { |
| | | private final ConcurrentHashMap<Integer, Task<?>> all; |
| | | |
| | | Executor(final int corePoolSize, final String prefix) { |
| | | super(corePoolSize, new ThreadFactory() { |
| | | private final ThreadFactory parent = Executors.defaultThreadFactory(); |
| | | private final AtomicInteger tid = new AtomicInteger(1); |
| | | |
| | | @Override |
| | | public Thread newThread(final Runnable task) { |
| | | final Thread t = parent.newThread(task); |
| | | t.setName(prefix + "-" + tid.getAndIncrement()); |
| | | t.setUncaughtExceptionHandler(LOG_UNCAUGHT_EXCEPTION); |
| | | return t; |
| | | } |
| | | }); |
| | | |
| | | all = new ConcurrentHashMap<Integer, Task<?>>( // |
| | | corePoolSize << 1, // table size |
| | | 0.75f, // load factor |
| | | corePoolSize + 4 // concurrency level |
| | | ); |
| | | } |
| | | |
| | | public void unregisterWorkQueue() { |
| | | queues.remove(this); |
| | | } |
| | | |
| | | @Override |
| | | protected <V> RunnableScheduledFuture<V> decorateTask( |
| | | final Runnable runnable, RunnableScheduledFuture<V> r) { |
| | | r = super.decorateTask(runnable, r); |
| | | for (;;) { |
| | | final int id = idGenerator.next(); |
| | | |
| | | Task<V> task; |
| | | task = new Task<V>(runnable, r, this, id); |
| | | |
| | | if (all.putIfAbsent(task.getTaskId(), task) == null) { |
| | | return task; |
| | | } |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | protected <V> RunnableScheduledFuture<V> decorateTask( |
| | | final Callable<V> callable, final RunnableScheduledFuture<V> task) { |
| | | throw new UnsupportedOperationException("Callable not implemented"); |
| | | } |
| | | |
| | | void remove(final Task<?> task) { |
| | | all.remove(task.getTaskId(), task); |
| | | } |
| | | |
| | | Task<?> getTask(final int id) { |
| | | return all.get(id); |
| | | } |
| | | |
| | | void addAllTo(final List<Task<?>> list) { |
| | | list.addAll(all.values()); // iterator is thread safe |
| | | } |
| | | |
| | | Collection<Task<?>> getTasks() { |
| | | return all.values(); |
| | | } |
| | | } |
| | | |
| | | /** Runnable needing to know it was canceled. */ |
| | | public interface CancelableRunnable extends Runnable { |
| | | /** Notifies the runnable it was canceled. */ |
| | | public void cancel(); |
| | | } |
| | | |
| | | /** A wrapper around a scheduled Runnable, as maintained in the queue. */ |
| | | public static class Task<V> implements RunnableScheduledFuture<V> { |
| | | /** |
| | | * Summarized status of a single task. |
| | | * <p> |
| | | * Tasks have the following state flow: |
| | | * <ol> |
| | | * <li>{@link #SLEEPING}: if scheduled with a non-zero delay.</li> |
| | | * <li>{@link #READY}: waiting for an available worker thread.</li> |
| | | * <li>{@link #RUNNING}: actively executing on a worker thread.</li> |
| | | * <li>{@link #DONE}: finished executing, if not periodic.</li> |
| | | * </ol> |
| | | */ |
| | | public static enum State { |
| | | // Ordered like this so ordinal matches the order we would |
| | | // prefer to see tasks sorted in: done before running, |
| | | // running before ready, ready before sleeping. |
| | | // |
| | | DONE, CANCELLED, RUNNING, READY, SLEEPING, OTHER |
| | | } |
| | | |
| | | private final Runnable runnable; |
| | | private final RunnableScheduledFuture<V> task; |
| | | private final Executor executor; |
| | | private final int taskId; |
| | | private final AtomicBoolean running; |
| | | private final Date startTime; |
| | | |
| | | Task(Runnable runnable, RunnableScheduledFuture<V> task, Executor executor, |
| | | int taskId) { |
| | | this.runnable = runnable; |
| | | this.task = task; |
| | | this.executor = executor; |
| | | this.taskId = taskId; |
| | | this.running = new AtomicBoolean(); |
| | | this.startTime = new Date(); |
| | | } |
| | | |
| | | public int getTaskId() { |
| | | return taskId; |
| | | } |
| | | |
| | | public State getState() { |
| | | if (isCancelled()) { |
| | | return State.CANCELLED; |
| | | } else if (isDone() && !isPeriodic()) { |
| | | return State.DONE; |
| | | } else if (running.get()) { |
| | | return State.RUNNING; |
| | | } |
| | | |
| | | final long delay = getDelay(TimeUnit.MILLISECONDS); |
| | | if (delay <= 0) { |
| | | return State.READY; |
| | | } else if (0 < delay) { |
| | | return State.SLEEPING; |
| | | } |
| | | |
| | | return State.OTHER; |
| | | } |
| | | |
| | | public Date getStartTime() { |
| | | return startTime; |
| | | } |
| | | |
| | | public boolean cancel(boolean mayInterruptIfRunning) { |
| | | if (task.cancel(mayInterruptIfRunning)) { |
| | | // Tiny abuse of running: if the task needs to know it was |
| | | // canceled (to clean up resources) and it hasn't started |
| | | // yet the task's run method won't execute. So we tag it |
| | | // as running and allow it to clean up. This ensures we do |
| | | // not invoke cancel twice. |
| | | // |
| | | if (runnable instanceof CancelableRunnable |
| | | && running.compareAndSet(false, true)) { |
| | | ((CancelableRunnable) runnable).cancel(); |
| | | } |
| | | executor.remove(this); |
| | | executor.purge(); |
| | | return true; |
| | | |
| | | } else { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | public int compareTo(Delayed o) { |
| | | return task.compareTo(o); |
| | | } |
| | | |
| | | public V get() throws InterruptedException, ExecutionException { |
| | | return task.get(); |
| | | } |
| | | |
| | | public V get(long timeout, TimeUnit unit) throws InterruptedException, |
| | | ExecutionException, TimeoutException { |
| | | return task.get(timeout, unit); |
| | | } |
| | | |
| | | public long getDelay(TimeUnit unit) { |
| | | return task.getDelay(unit); |
| | | } |
| | | |
| | | public boolean isCancelled() { |
| | | return task.isCancelled(); |
| | | } |
| | | |
| | | public boolean isDone() { |
| | | return task.isDone(); |
| | | } |
| | | |
| | | public boolean isPeriodic() { |
| | | return task.isPeriodic(); |
| | | } |
| | | |
| | | public void run() { |
| | | if (running.compareAndSet(false, true)) { |
| | | try { |
| | | task.run(); |
| | | } finally { |
| | | if (isPeriodic()) { |
| | | running.set(false); |
| | | } else { |
| | | executor.remove(this); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return runnable.toString(); |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | /* |
| | | * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> |
| | | * |
| | | * (Taken from JGit org.eclipse.jgit.pgm.opt.CmdLineParser.) |
| | | * |
| | | * All rights reserved. |
| | | * |
| | | * Redistribution and use in source and binary forms, with or without |
| | | * modification, are permitted provided that the following conditions are met: |
| | | * |
| | | * - Redistributions of source code must retain the above copyright notice, this |
| | | * list of conditions and the following disclaimer. |
| | | * |
| | | * - Redistributions in binary form must reproduce the above copyright notice, |
| | | * this list of conditions and the following disclaimer in the documentation |
| | | * and/or other materials provided with the distribution. |
| | | * |
| | | * - Neither the name of the Git Development Community nor the names of its |
| | | * contributors may be used to endorse or promote products derived from this |
| | | * software without specific prior written permission. |
| | | * |
| | | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| | | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| | | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| | | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE |
| | | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| | | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| | | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| | | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| | | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| | | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
| | | * POSSIBILITY OF SUCH DAMAGE. |
| | | */ |
| | | |
| | | package com.gitblit.utils.cli; |
| | | |
| | | import java.io.StringWriter; |
| | | import java.io.Writer; |
| | | import java.lang.annotation.Annotation; |
| | | import java.lang.reflect.AnnotatedElement; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.ResourceBundle; |
| | | |
| | | import org.kohsuke.args4j.Argument; |
| | | import org.kohsuke.args4j.CmdLineException; |
| | | import org.kohsuke.args4j.IllegalAnnotationError; |
| | | import org.kohsuke.args4j.NamedOptionDef; |
| | | import org.kohsuke.args4j.Option; |
| | | import org.kohsuke.args4j.OptionDef; |
| | | import org.kohsuke.args4j.spi.BooleanOptionHandler; |
| | | import org.kohsuke.args4j.spi.EnumOptionHandler; |
| | | import org.kohsuke.args4j.spi.FieldSetter; |
| | | import org.kohsuke.args4j.spi.OptionHandler; |
| | | import org.kohsuke.args4j.spi.Setter; |
| | | |
| | | import com.google.common.base.Strings; |
| | | import com.google.common.collect.LinkedHashMultimap; |
| | | import com.google.common.collect.Lists; |
| | | import com.google.common.collect.Maps; |
| | | import com.google.common.collect.Multimap; |
| | | |
| | | /** |
| | | * Extended command line parser which handles --foo=value arguments. |
| | | * <p> |
| | | * The args4j package does not natively handle --foo=value and instead prefers |
| | | * to see --foo value on the command line. Many users are used to the GNU style |
| | | * --foo=value long option, so we convert from the GNU style format to the |
| | | * args4j style format prior to invoking args4j for parsing. |
| | | */ |
| | | public class CmdLineParser { |
| | | public interface Factory { |
| | | CmdLineParser create(Object bean); |
| | | } |
| | | |
| | | private final MyParser parser; |
| | | |
| | | @SuppressWarnings("rawtypes") |
| | | private Map<String, OptionHandler> options; |
| | | |
| | | /** |
| | | * Creates a new command line owner that parses arguments/options and set them |
| | | * into the given object. |
| | | * |
| | | * @param bean instance of a class annotated by |
| | | * {@link org.kohsuke.args4j.Option} and |
| | | * {@link org.kohsuke.args4j.Argument}. this object will receive |
| | | * values. |
| | | * |
| | | * @throws IllegalAnnotationError if the option bean class is using args4j |
| | | * annotations incorrectly. |
| | | */ |
| | | public CmdLineParser(Object bean) |
| | | throws IllegalAnnotationError { |
| | | this.parser = new MyParser(bean); |
| | | } |
| | | |
| | | public void addArgument(Setter<?> setter, Argument a) { |
| | | parser.addArgument(setter, a); |
| | | } |
| | | |
| | | public void addOption(Setter<?> setter, Option o) { |
| | | parser.addOption(setter, o); |
| | | } |
| | | |
| | | public void printSingleLineUsage(Writer w, ResourceBundle rb) { |
| | | parser.printSingleLineUsage(w, rb); |
| | | } |
| | | |
| | | public void printUsage(Writer out, ResourceBundle rb) { |
| | | parser.printUsage(out, rb); |
| | | } |
| | | |
| | | public void printDetailedUsage(String name, StringWriter out) { |
| | | out.write(name); |
| | | printSingleLineUsage(out, null); |
| | | out.write('\n'); |
| | | out.write('\n'); |
| | | printUsage(out, null); |
| | | out.write('\n'); |
| | | } |
| | | |
| | | public void printQueryStringUsage(String name, StringWriter out) { |
| | | out.write(name); |
| | | |
| | | char next = '?'; |
| | | List<NamedOptionDef> booleans = new ArrayList<NamedOptionDef>(); |
| | | for (@SuppressWarnings("rawtypes") OptionHandler handler : parser.options) { |
| | | if (handler.option instanceof NamedOptionDef) { |
| | | NamedOptionDef n = (NamedOptionDef) handler.option; |
| | | |
| | | if (handler instanceof BooleanOptionHandler) { |
| | | booleans.add(n); |
| | | continue; |
| | | } |
| | | |
| | | if (!n.required()) { |
| | | out.write('['); |
| | | } |
| | | out.write(next); |
| | | next = '&'; |
| | | if (n.name().startsWith("--")) { |
| | | out.write(n.name().substring(2)); |
| | | } else if (n.name().startsWith("-")) { |
| | | out.write(n.name().substring(1)); |
| | | } else { |
| | | out.write(n.name()); |
| | | } |
| | | out.write('='); |
| | | |
| | | out.write(metaVar(handler, n)); |
| | | if (!n.required()) { |
| | | out.write(']'); |
| | | } |
| | | if (n.isMultiValued()) { |
| | | out.write('*'); |
| | | } |
| | | } |
| | | } |
| | | for (NamedOptionDef n : booleans) { |
| | | if (!n.required()) { |
| | | out.write('['); |
| | | } |
| | | out.write(next); |
| | | next = '&'; |
| | | if (n.name().startsWith("--")) { |
| | | out.write(n.name().substring(2)); |
| | | } else if (n.name().startsWith("-")) { |
| | | out.write(n.name().substring(1)); |
| | | } else { |
| | | out.write(n.name()); |
| | | } |
| | | if (!n.required()) { |
| | | out.write(']'); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private static String metaVar(OptionHandler<?> handler, NamedOptionDef n) { |
| | | String var = n.metaVar(); |
| | | if (Strings.isNullOrEmpty(var)) { |
| | | var = handler.getDefaultMetaVariable(); |
| | | if (handler instanceof EnumOptionHandler) { |
| | | var = var.substring(1, var.length() - 1).replace(" ", ""); |
| | | } |
| | | } |
| | | return var; |
| | | } |
| | | |
| | | public boolean wasHelpRequestedByOption() { |
| | | return parser.help.value; |
| | | } |
| | | |
| | | public void parseArgument(final String... args) throws CmdLineException { |
| | | List<String> tmp = Lists.newArrayListWithCapacity(args.length); |
| | | for (int argi = 0; argi < args.length; argi++) { |
| | | final String str = args[argi]; |
| | | if (str.equals("--")) { |
| | | while (argi < args.length) |
| | | tmp.add(args[argi++]); |
| | | break; |
| | | } |
| | | |
| | | if (str.startsWith("--")) { |
| | | final int eq = str.indexOf('='); |
| | | if (eq > 0) { |
| | | tmp.add(str.substring(0, eq)); |
| | | tmp.add(str.substring(eq + 1)); |
| | | continue; |
| | | } |
| | | } |
| | | |
| | | tmp.add(str); |
| | | } |
| | | parser.parseArgument(tmp.toArray(new String[tmp.size()])); |
| | | } |
| | | |
| | | public void parseOptionMap(Map<String, String[]> parameters) |
| | | throws CmdLineException { |
| | | Multimap<String, String> map = LinkedHashMultimap.create(); |
| | | for (Map.Entry<String, String[]> ent : parameters.entrySet()) { |
| | | for (String val : ent.getValue()) { |
| | | map.put(ent.getKey(), val); |
| | | } |
| | | } |
| | | parseOptionMap(map); |
| | | } |
| | | |
| | | public void parseOptionMap(Multimap<String, String> params) |
| | | throws CmdLineException { |
| | | List<String> tmp = Lists.newArrayListWithCapacity(2 * params.size()); |
| | | for (final String key : params.keySet()) { |
| | | String name = makeOption(key); |
| | | |
| | | if (isBoolean(name)) { |
| | | boolean on = false; |
| | | for (String value : params.get(key)) { |
| | | on = toBoolean(key, value); |
| | | } |
| | | if (on) { |
| | | tmp.add(name); |
| | | } |
| | | } else { |
| | | for (String value : params.get(key)) { |
| | | tmp.add(name); |
| | | tmp.add(value); |
| | | } |
| | | } |
| | | } |
| | | parser.parseArgument(tmp.toArray(new String[tmp.size()])); |
| | | } |
| | | |
| | | public boolean isBoolean(String name) { |
| | | return findHandler(makeOption(name)) instanceof BooleanOptionHandler; |
| | | } |
| | | |
| | | private String makeOption(String name) { |
| | | if (!name.startsWith("-")) { |
| | | if (name.length() == 1) { |
| | | name = "-" + name; |
| | | } else { |
| | | name = "--" + name; |
| | | } |
| | | } |
| | | return name; |
| | | } |
| | | |
| | | @SuppressWarnings("rawtypes") |
| | | private OptionHandler findHandler(String name) { |
| | | if (options == null) { |
| | | options = index(parser.options); |
| | | } |
| | | return options.get(name); |
| | | } |
| | | |
| | | @SuppressWarnings("rawtypes") |
| | | private static Map<String, OptionHandler> index(List<OptionHandler> in) { |
| | | Map<String, OptionHandler> m = Maps.newHashMap(); |
| | | for (OptionHandler handler : in) { |
| | | if (handler.option instanceof NamedOptionDef) { |
| | | NamedOptionDef def = (NamedOptionDef) handler.option; |
| | | if (!def.isArgument()) { |
| | | m.put(def.name(), handler); |
| | | for (String alias : def.aliases()) { |
| | | m.put(alias, handler); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return m; |
| | | } |
| | | |
| | | private boolean toBoolean(String name, String value) throws CmdLineException { |
| | | if ("true".equals(value) || "t".equals(value) |
| | | || "yes".equals(value) || "y".equals(value) |
| | | || "on".equals(value) |
| | | || "1".equals(value) |
| | | || value == null || "".equals(value)) { |
| | | return true; |
| | | } |
| | | |
| | | if ("false".equals(value) || "f".equals(value) |
| | | || "no".equals(value) || "n".equals(value) |
| | | || "off".equals(value) |
| | | || "0".equals(value)) { |
| | | return false; |
| | | } |
| | | |
| | | throw new CmdLineException(parser, String.format( |
| | | "invalid boolean \"%s=%s\"", name, value)); |
| | | } |
| | | |
| | | private class MyParser extends org.kohsuke.args4j.CmdLineParser { |
| | | @SuppressWarnings("rawtypes") |
| | | private List<OptionHandler> options; |
| | | private HelpOption help; |
| | | |
| | | MyParser(final Object bean) { |
| | | super(bean); |
| | | ensureOptionsInitialized(); |
| | | } |
| | | |
| | | @SuppressWarnings({"unchecked", "rawtypes"}) |
| | | @Override |
| | | protected OptionHandler createOptionHandler(final OptionDef option, |
| | | final Setter setter) { |
| | | if (isHandlerSpecified(option) || isEnum(setter) || isPrimitive(setter)) { |
| | | return add(super.createOptionHandler(option, setter)); |
| | | } |
| | | |
| | | // OptionHandlerFactory<?> factory = handlers.get(setter.getType()); |
| | | // if (factory != null) { |
| | | // return factory.create(this, option, setter); |
| | | // } |
| | | return add(super.createOptionHandler(option, setter)); |
| | | } |
| | | |
| | | @SuppressWarnings("rawtypes") |
| | | private OptionHandler add(OptionHandler handler) { |
| | | ensureOptionsInitialized(); |
| | | options.add(handler); |
| | | return handler; |
| | | } |
| | | |
| | | private void ensureOptionsInitialized() { |
| | | if (options == null) { |
| | | help = new HelpOption(); |
| | | options = Lists.newArrayList(); |
| | | addOption(help, help); |
| | | } |
| | | } |
| | | |
| | | private boolean isHandlerSpecified(final OptionDef option) { |
| | | return option.handler() != OptionHandler.class; |
| | | } |
| | | |
| | | private <T> boolean isEnum(Setter<T> setter) { |
| | | return Enum.class.isAssignableFrom(setter.getType()); |
| | | } |
| | | |
| | | private <T> boolean isPrimitive(Setter<T> setter) { |
| | | return setter.getType().isPrimitive(); |
| | | } |
| | | } |
| | | |
| | | private static class HelpOption implements Option, Setter<Boolean> { |
| | | private boolean value; |
| | | |
| | | @Override |
| | | public String name() { |
| | | return "--help"; |
| | | } |
| | | |
| | | @Override |
| | | public String[] aliases() { |
| | | return new String[] {"-h"}; |
| | | } |
| | | |
| | | @Override |
| | | public String[] depends() { |
| | | return new String[] {}; |
| | | } |
| | | |
| | | @Override |
| | | public boolean hidden() { |
| | | return false; |
| | | } |
| | | |
| | | @Override |
| | | public String usage() { |
| | | return "display this help text"; |
| | | } |
| | | |
| | | @Override |
| | | public void addValue(Boolean val) { |
| | | value = val; |
| | | } |
| | | |
| | | @Override |
| | | public Class<? extends OptionHandler<Boolean>> handler() { |
| | | return BooleanOptionHandler.class; |
| | | } |
| | | |
| | | @Override |
| | | public String metaVar() { |
| | | return ""; |
| | | } |
| | | |
| | | @Override |
| | | public boolean required() { |
| | | return false; |
| | | } |
| | | |
| | | @Override |
| | | public Class<? extends Annotation> annotationType() { |
| | | return Option.class; |
| | | } |
| | | |
| | | @Override |
| | | public FieldSetter asFieldSetter() { |
| | | throw new UnsupportedOperationException(); |
| | | } |
| | | |
| | | @Override |
| | | public AnnotatedElement asAnnotatedElement() { |
| | | throw new UnsupportedOperationException(); |
| | | } |
| | | |
| | | @Override |
| | | public Class<Boolean> getType() { |
| | | return Boolean.class; |
| | | } |
| | | |
| | | @Override |
| | | public boolean isMultiValued() { |
| | | return false; |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | // Copyright (C) 2010 The Android Open Source Project |
| | | // |
| | | // 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.gitblit.utils.cli; |
| | | |
| | | import org.kohsuke.args4j.CmdLineException; |
| | | import org.kohsuke.args4j.CmdLineParser; |
| | | import org.kohsuke.args4j.OptionDef; |
| | | import org.kohsuke.args4j.spi.OptionHandler; |
| | | import org.kohsuke.args4j.spi.Parameters; |
| | | import org.kohsuke.args4j.spi.Setter; |
| | | |
| | | public class SubcommandHandler extends OptionHandler<String> { |
| | | |
| | | public SubcommandHandler(CmdLineParser parser, |
| | | OptionDef option, Setter<String> setter) { |
| | | super(parser, option, setter); |
| | | } |
| | | |
| | | @Override |
| | | public final int parseArguments(final Parameters params) |
| | | throws CmdLineException { |
| | | setter.addValue(params.getParameter(0)); |
| | | owner.stopOptionParsing(); |
| | | return 1; |
| | | } |
| | | |
| | | @Override |
| | | public final String getDefaultMetaVariable() { |
| | | return "COMMAND"; |
| | | } |
| | | } |
| | |
| | | #log4j.logger.net=INFO |
| | | |
| | | #log4j.logger.com.gitblit=DEBUG |
| | | log4j.logger.org.apache.sshd=ERROR |
| | | |
| | | log4j.logger.org.apache.wicket=INFO |
| | | log4j.logger.org.apache.wicket.RequestListenerInterface=WARN |