usr/src/java/vpanels/client/org/opensolaris/os/vp/client/swing/App.java
author Stephen Talley <stephen.talley@oracle.com>
Mon, 28 Mar 2011 10:53:34 -0400
changeset 685 767674b0a2fb
parent 651 eeb57de7d602
child 710 beb915128edf
permissions -rw-r--r--
18094 s/StringBuffer/StringBuilder/g

/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright (c) 2009, 2011, Oracle and/or its affiliates. All rights reserved.
 */

package org.opensolaris.os.vp.client.swing;

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.logging.*;
import org.opensolaris.os.uds.*;
import org.opensolaris.os.vp.client.common.*;
import org.opensolaris.os.vp.panel.common.*;
import org.opensolaris.os.vp.panel.common.action.*;
import org.opensolaris.os.vp.panel.common.control.*;
import org.opensolaris.os.vp.util.cli.*;
import org.opensolaris.os.vp.util.misc.*;
import org.opensolaris.os.vp.util.misc.finder.Finder;

/**
 * The {@code App} class is a thread that listens for UDS connections from other
 * processes.  Multiple addresses of panels to display, one per
 * newline-terminated lines, may be sent through these connections.
 */
public class App extends Thread implements AppConstants {
    //
    // Inner classes
    //

    public static class CommandLineOptionsBean {
	//
	// Instance data
	//

	private String host = RadLoginManager.LOCAL_HOST;
	private String user = RadLoginManager.LOCAL_USER;
	private String role;
	private String zone;
	private String zoneUser;
	private String zoneRole;
	private String address = null;
	private boolean noRemote;
	private HelpFormatter help;
	private Properties properties = new Properties();
	private boolean inParam = false;

	//
	// Constructors
	//

	public CommandLineOptionsBean(HelpFormatter help) {
	    this.help = help;
	}

	//
	// CommandLineOptionsBean methods
	//

	public String getAddress() {
	    return address;
	}

	public String getHost() {
	    return host;
	}

	public boolean getNoremote() {
	    return noRemote;
	}

	public Properties getProperties() {
	    return properties;
	}

	public String getRole() {
	    return role;
	}

	public String getUser() {
	    return user;
	}

	public String getZone() {
	    return zone;
	}

	public String getZonerole() {
	    return zoneRole;
	}

	public String getZoneuser() {
	    return zoneUser;
	}

	public void setAddress(String address) {
	    boolean absolute = address.startsWith("/");
	    if (this.address == null) {
		if (!absolute) {
		    // Assume non-relative path is a standalone shortcut
		    address = String.format("/%s/%s",
			Control.encode(AppRootControl.ID, null), address);
		}
		this.address = address;
	    } else {
		/*
		 * If the address has already been set, we accumulate
		 * additional operands as query parameters or subsequent
		 * path elements.
		 */
		if (absolute) {
		    this.address = this.address + address;
		    inParam = false;
		} else {
		    try {
			/*
			 * Ideally we'd use Control.encode, but mapping
			 * from our input to what it requires would be
			 * much less straightforward than what follows.
			 */
			String[] parts = address.split("=", 2);
			StringBuilder b = new StringBuilder(this.address);
			b.append(inParam ? "&" : "?");
			b.append(URLEncoder.encode(parts[0], Control.ENCODING));
			b.append('=');
			if (parts.length > 1)
			    b.append(URLEncoder.encode(parts[1],
				Control.ENCODING));
			this.address = b.toString();
			inParam = true;
		    } catch (UnsupportedEncodingException ex) {
		    }
		}
	    }
	}

	public void setHelp() {
	    System.out.println(help.getHelp());
	    System.exit(0);
	}

	public void setHost(String host) {
	    this.host = host;
	}

	public void setNoremote() {
	    noRemote = true;
	}

	public void setProperty(String property) {
	    String value;
	    int index = property.indexOf("=");
	    if (index == -1) {
		value = "";
	    } else {
		value = property.substring(index + 1);
		property = property.substring(0, index - 1);
	    }

	    properties.setProperty(property, value);
	}

	public void setRole(String role) {
	    this.role = role;
	}

	public void setUser(String user) {
	    this.user = user;
	}

	public void setVersion() {
	    showVersion();
	    System.exit(0);
	}

	public void setZone(String zone) {
	    this.zone = zone;
	}

	public void setZonerole(String zoneRole) {
	    this.zoneRole = zoneRole;
	}

	public void setZoneuser(String zoneUser) {
	    this.zoneUser = zoneUser;
	}
    }

    private static class InstanceInfo implements Serializable {
	//
	// Instance data
	//

	public String address;
	public Properties properties;
	public String host;
	public String user;
	public String role;
	public String zone;
	public String zoneUser;
	public String zoneRole;

	//
	// Constructors
	//

        public InstanceInfo(String address, Properties properties, String host,
            String user, String role, String zone, String zoneUser,
	    String zoneRole) {

	    this.address = address;
	    this.properties = properties;
	    this.host = host;
	    this.user = user;
	    this.role = role;
	    this.zone = zone;
	    this.zoneUser = zoneUser;
	    this.zoneRole = zoneRole;
	}

	//
	// Object methods
	//

	@Override
	public String toString() {
	    return "address = " + quote(address) +
		", host = " + quote(host) +
		", user = " + quote(user) +
		", role = " + quote(role) +
		", zone = " + quote(zone) +
		", zoneUser = " + quote(zoneUser) +
		", zoneRole = " + quote(zoneRole);
	}

	//
	// Static methods
	//

	private static String quote(String str) {
	    if (str == null) {
		return "(null)";
	    }
	    return "\"" + str + "\"";
	}
    }

    //
    // Static data
    //

    private static final String ARG_NONOPT_ADDRESS = "address";
    private static final String ARG_SHORT_HOST = "h";
    private static final String ARG_LONG_HOST = "host";
    private static final String ARG_SHORT_NOREMOTE = "n";
    private static final String ARG_LONG_NOREMOTE = "no-remote";
    private static final String ARG_SHORT_ROLE = "r";
    private static final String ARG_LONG_ROLE = "role";
    private static final String ARG_SHORT_USER = "u";
    private static final String ARG_LONG_USER = "user";
    private static final String ARG_SHORT_VERSION = "v";
    private static final String ARG_LONG_VERSION = "version";
    private static final String ARG_SHORT_ZONE = "z";
    private static final String ARG_LONG_ZONE = "zone";
    private static final String ARG_SHORT_ZONEUSER = "U";
    private static final String ARG_LONG_ZONEUSER = "zoneuser";
    private static final String ARG_SHORT_ZONEROLE = "R";
    private static final String ARG_LONG_ZONEROLE = "zonerole";
    private static final String ARG_SHORT_HELP = "?";
    private static final String ARG_LONG_HELP = "help";
    private static final String ARG_SHORT_PROPERTY = "D";
    private static final String ARG_LONG_PROPERTY = "property";

    private static final String COMMAND_NAME = "vp";
    private static final String COMMAND_DESC =
	Finder.getString("cli.description");

    private static OptionChoiceGroup options;
    static {
	OptionElement addressOption = new NoOptOptionElement(
	    true, ARG_NONOPT_ADDRESS, Finder.getString("cli.arg.address"), -1);

	OptionElement propOption = new OptionElement(ARG_SHORT_PROPERTY,
	    ARG_LONG_PROPERTY, false, "property", "");
	propOption.setDocumented(false);
	propOption.setUseLimit(-1);

	OptionElement hostOption = new OptionElement(ARG_SHORT_HOST,
	    ARG_LONG_HOST, false, "host", Finder.getString("cli.arg.host"));

	OptionElement noRemoteOption = new OptionElement(ARG_SHORT_NOREMOTE,
	    ARG_LONG_NOREMOTE, false, Finder.getString("cli.arg.noremote"));

	OptionElement userOption = new OptionElement(ARG_SHORT_USER,
	    ARG_LONG_USER, false, "user", Finder.getString("cli.arg.user"));

	OptionElement roleOption = new OptionElement(ARG_SHORT_ROLE,
	    ARG_LONG_ROLE, false, "role", Finder.getString("cli.arg.role"));

	OptionElement zoneOption = new OptionElement(ARG_SHORT_ZONE,
	    ARG_LONG_ZONE, false, "zone", Finder.getString("cli.arg.zone"));

	OptionElement zoneUserOption = new OptionElement(ARG_SHORT_ZONEUSER,
	    ARG_LONG_ZONEUSER, false, "zoneuser",
	    Finder.getString("cli.arg.zoneuser"));

	OptionElement zoneRoleOption = new OptionElement(ARG_SHORT_ZONEROLE,
	    ARG_LONG_ZONEROLE, false, "zonerole",
	    Finder.getString("cli.arg.zonerole"));

	OptionElement versionOption = new OptionElement(ARG_SHORT_VERSION,
	    ARG_LONG_VERSION, false, Finder.getString("cli.arg.version"));

	OptionElement helpOption = new OptionElement(ARG_SHORT_HELP,
	    ARG_LONG_HELP, false, Finder.getString("cli.arg.help"));

	OptionListGroup mainGroup = new OptionListGroup(false,
	    propOption, hostOption, userOption, roleOption, zoneOption,
	    zoneUserOption, zoneRoleOption, noRemoteOption, addressOption);

	options = new OptionChoiceGroup(true, mainGroup, versionOption,
	    helpOption);
    }

    public static final String VP_USER_DIR =
	System.getProperty("user.home") + File.separator + ".vp";

    static {
	// Set up client logging
	String fileName = VP_USER_DIR + File.separator + "log";

	// Root Logger
	Logger log = Logger.getLogger("");

	try {
	    File parent = new File(fileName).getParentFile();
	    if (!parent.exists()) {
		if (!parent.mkdirs()) {
		    throw new IOException(
			"could not create directory: " +
			parent.getAbsolutePath());
		}
	    }

	    Handler fHandler = new FileHandler(fileName);
	    fHandler.setFormatter(new SimpleFormatter());
	    log.addHandler(fHandler);

	    // Don't output to the console by default
	    for (Handler cHandler : log.getHandlers()) {
		if (cHandler instanceof ConsoleHandler) {
		    log.removeHandler(cHandler);
		}
	    }
	} catch (IOException e) {
	    Logger.getLogger(App.class.getName()).log(Level.WARNING,
		"unable to create log file: " + fileName);
	}
    }

    public static final File TRUSTSTORE_FILE =
	new File(VP_USER_DIR, "truststore");

    public static final File VP_UDS;
    static {
	String hostName = NetUtil.getHostName();
	if (hostName == null) {
	    hostName = "localhost";
	}

	// May be null or malformed if not an X client
	String display = System.getenv("DISPLAY");

	if (display != null && !display.matches(".*:\\d+(\\.\\d)?$")) {
	    String error = "invalid DISPLAY environment variable: " + display;
	    Logger.getLogger(App.class.getName()).log(Level.WARNING, error);
	    display = null;
	}

	if (display == null) {
	    display = ":0.0";
	} else {
	    if (display.matches(".*:\\d+$")) {
		display += ".0";
	    }

	    if (display.startsWith(hostName)) {
		display = display.substring(hostName.length());
	    }
	}

	String name = String.format(".uds-%s-%s", hostName, display);

	VP_UDS = new File(VP_USER_DIR, name);
    }

    public static final String VP_USER_PREFS =
	VP_USER_DIR + File.separator + "vp.init";

    private static final Preferences prefs = new Preferences(VP_USER_PREFS);

    //
    // Instance data
    //

    private UDSocketServer server;
    private boolean closing;
    private final List<AppInstance> instances = new ArrayList<AppInstance>();
    private ConnectionManager connManager = new ConnectionManager();
    private LoginHistoryManager loginHistoryManager =
	new LoginHistoryManager(connManager, new File(VP_USER_DIR, "history"));

    //
    // Constructors
    //

    public App() {
	addVersionToThreadName(this);
    }

    //
    // Runnable methods
    //

    @Override
    public void run() {
        Logger log = Logger.getLogger(getClass().getName());

	// Start singleton application instance...
	try {
	    server = UnixDomainSocket.bind(VP_UDS, 0600);

	    while (true) {
		final UDSocket socket = server.accept();
		String peerUser = socket.getPeerUser();

		if (!RadLoginManager.LOCAL_USER.equals(peerUser)) {
		    String error = String.format(
			"user %s attempted to connect as %s", peerUser,
			RadLoginManager.LOCAL_USER);
		    log.log(Level.WARNING, error);
		} else {
		    // Spawn on a separate thread so as not to block other
		    // incoming to conections -- not so much because we
		    // anticipate a lot of traffic, but because we can't
		    // guarantee that some poorly-behaved client won't keep its
		    // connection open indefinitely.
		    new Thread() {
			@Override
			public void run() {
			    readFully(socket);
			}
		    }.start();
		}
	    }
	} catch (UDSNotSupportedException e) {
	    // Bummer
	    return;
	} catch (IOException e) {
	    if (!closing) {
		// Major bummer
		log.log(Level.SEVERE,
		    "error communicating via incoming uds connection", e);
	    }
	    return;
	}
    }

    //
    // App methods
    //

    public void abortableExit() throws ActionAbortedException {
	synchronized (instances) {
	    for (int i = instances.size() - 1; i >= 0; i--) {
		// Throws ActionAbortedException
		instances.get(i).closeInstance(false);
	    }
	}
	exit();
    }

    public void exit(int exitCode) {
	try {
	    if (server != null) {
		closing = true;
		server.close();
	    }
	} catch (IOException ignore) {
	}

	try {
	    prefs.store();
	} catch (IOException e) {
	    Logger.getLogger(getClass().getName()).log(Level.WARNING,
		"could not write preferences", e);
	}

	System.exit(exitCode);
    }

    public void exit() {
	exit(0);
    }

    private void exitIfNoIntances(int exitCode) {
	if (instances.isEmpty()) {
	    exit(exitCode);
	}
    }

    public ConnectionManager getConnectionManager() {
	return connManager;
    }

    public LoginHistoryManager getLoginHistoryManager() {
	return loginHistoryManager;
    }

    protected Preferences getPreferences() {
	return prefs;
    }

    protected void instanceClosed(AppInstance instance) {
	synchronized (instances) {
	    if (instances.remove(instance)) {
		exitIfNoIntances(0);
	    }
	}
    }

    protected void instanceCreated(AppInstance instance) {
	synchronized (instances) {
	    instances.add(instance);
	}
    }

    public void newInstance(InstanceInfo info) {
	AppInstance instance = null;
	boolean success = false;

	try {
	    LoginProperty<String> host =
		new LoginProperty<String>(info.host, false);
	    host.setEditableOnError(true);

	    LoginProperty<String> user =
		new LoginProperty<String>(info.user, false);
	    user.setEditableOnError(true);

	    LoginProperty<String> role =
		new LoginProperty<String>(info.role, false);
	    role.setEditableOnError(true);

	    LoginProperty<String> zone =
		new LoginProperty<String>(info.zone, false);
	    zone.setEditableOnError(true);

	    LoginProperty<String> zoneUser =
		new LoginProperty<String>(info.zoneUser, false);
	    zoneUser.setEditableOnError(true);

	    LoginProperty<String> zoneRole =
		new LoginProperty<String>(info.zoneRole, false);
	    zoneRole.setEditableOnError(true);

            boolean zonePromptVal = zone.getValue() != null ||
		zoneUser.getValue() != null || zoneRole.getValue() != null;
	    LoginProperty<Boolean> zonePrompt =
		new LoginProperty<Boolean>(zonePromptVal, false);

            LoginRequest request = new LoginRequest(host, user, role,
		zonePrompt, zone, zoneUser, zoneRole);

	    instance = new AppInstance(this, info.properties, request);

	    SimpleNavigable[] path = Navigator.toArray(info.address);
	    instance.getNavigator().goToAsyncAndWait(true, null, path);
	    success = true;

	// User is aware of this because he explicitly caused it
	} catch (ActionAbortedException e) {

	// User has been advised of this by RadLoginManager
	} catch (ActionFailedException e) {

	// User has been advised of this by SwingNavigator
	} catch (NavigationException e) {

	// Unexpected error - write to log
	} catch (RuntimeException e) {
	    Logger log = Logger.getLogger(getClass().getName());
	    log.log(Level.SEVERE, "could not launch: " + info, e);

	// Unexpected error - write to log
	} catch (Error e) {
	    Logger log = Logger.getLogger(getClass().getName());
	    log.log(Level.SEVERE, "could not launch: " + info, e);

	} finally {
	    if (!success && instance != null &&
		// Partial navigation failures aren't deal-breakers
		!NavigationUtil.isPanelStarted(instance.getNavigator())) {

		instance.close();
	    }
	}
    }

    public void readFully(UDSocket socket) {
	ObjectInputStream in = null;
        Logger log = Logger.getLogger(getClass().getName());

	try {
	    in = new ObjectInputStream(socket.getInputStream());
	    InstanceInfo info = (InstanceInfo)in.readObject();
	    newInstance(info);
	} catch (ClassNotFoundException e) {
            log.log(Level.SEVERE, "unexpected or invalid object read from UDS",
		e);

	} catch (IOException e) {
	    log.log(Level.SEVERE, "I/O error", e);

	} finally {
	    IOUtil.closeIgnore(in);
	}
    }

    //
    // Static methods
    //

    private static void addVersionToThreadName(Thread thread) {
	// Add useful information to a thread dump
	String name = String.format(
	    "%s (app version %s)", thread.getName(), VERSION);

	thread.setName(name);
    }

    public static void showVersion() {
	System.out.println(Finder.getString("cli.error.version", VERSION));
    }

    public static void main(String args[]) {
	addVersionToThreadName(Thread.currentThread());

	CommandLineParser parser = new PosixCommandLineParser();
	UsageFormatter usage = new UsageFormatter(
	    COMMAND_NAME, options, parser.getOptionFormatter());
	HelpFormatter help = new HelpFormatter(COMMAND_DESC, usage);
	CommandLineOptionsBean bean = new CommandLineOptionsBean(help);

	if (System.getProperty("vpanels.debug.version") != null) {
	    showVersion();
	}

	try {
	    // Populate bean
	    CommandLineProcessor.process(args, options, parser, bean);
	}

	catch (Exception e) {
	    CommandUtil.exit(e, usage);
	}

        InstanceInfo info = new InstanceInfo(bean.getAddress(),
            bean.getProperties(), bean.getHost(), bean.getUser(),
            bean.getRole(), bean.getZone(), bean.getZoneuser(),
            bean.getZonerole());

	if (!bean.getNoremote()) {
	    // Check for running instance
	    try {
		UDSocket socket = UnixDomainSocket.connect(VP_UDS);

		// Send arguments to running instance...
		ObjectOutputStream out = new ObjectOutputStream(
		    socket.getOutputStream());

		out.writeObject(info);
		out.close();
		System.exit(0);
	    } catch (UDSNotSupportedException e) {
		// UDS not supported on this platform
	    } catch (IOException e) {
		// We are the only running instance
	    }
	}

	try {
	    URL policy = Finder.getResource("panel.policy");
	    if (policy == null) {
		throw new IOException("panel.policy not found");
	    }
	    PanelClassLoader.loadPermissions(policy);
	} catch (IOException e) {
	    Logger.getLogger(App.class.getName()).log(Level.SEVERE,
		"unable to read panel.policy", e);
	    String message = Finder.getString("init.error.security.io");
	    System.err.println(message);
	    System.exit(1);
	} catch (PermissionParseException e) {
	    Logger.getLogger(App.class.getName()).log(Level.SEVERE,
		"unable to parse panel.policy", e);
	    String message = Finder.getString("init.error.security.parse");
	    System.err.println(message);
	    System.exit(1);
	}

	System.setSecurityManager(new SecurityManager());

	App app = new App();
	app.newInstance(info);
	app.exitIfNoIntances(1);

	if (!bean.getNoremote()) {
	    // Start daemon thread listening for connections
	    app.start();
	}
    }
}