package org.jenkinsci.plugins.p4_client.client; import hudson.model.TaskListener; import hudson.security.ACL; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.logging.Logger; import jenkins.model.Jenkins; import org.acegisecurity.Authentication; import org.jenkinsci.plugins.p4_client.credentials.P4StandardCredentials; import org.jenkinsci.plugins.p4_client.workspace.Workspace; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.domains.DomainRequirement; import com.perforce.p4java.client.IClient; import com.perforce.p4java.core.IChangelistSummary; import com.perforce.p4java.core.file.FileAction; import com.perforce.p4java.core.file.FileSpecBuilder; import com.perforce.p4java.core.file.FileSpecOpStatus; import com.perforce.p4java.core.file.IFileSpec; import com.perforce.p4java.impl.generic.client.ClientOptions; import com.perforce.p4java.impl.generic.core.Changelist; import com.perforce.p4java.option.client.ReconcileFilesOptions; import com.perforce.p4java.option.client.RevertFilesOptions; import com.perforce.p4java.option.client.SyncOptions; import com.perforce.p4java.option.server.GetChangelistsOptions; import com.perforce.p4java.option.server.GetFileContentsOptions; import com.perforce.p4java.server.IOptionsServer; public class ConnectionHelper { private static Logger logger = Logger.getLogger(ConnectionHelper.class .getName()); private final ConnectionConfig connectionConfig; private final AuthorisationConfig authorisationConfig; private final IOptionsServer connection; private final TaskListener listener; private IClient iclient; public ConnectionHelper(String credentialID, TaskListener listener) throws Exception { this.listener = listener; P4StandardCredentials credential = findCredential(credentialID); this.connectionConfig = new ConnectionConfig(credential); this.authorisationConfig = new AuthorisationConfig(credential); this.connection = ConnectionFactory.getConnection(connectionConfig); } public ConnectionHelper(P4StandardCredentials credential) throws Exception { this.listener = null; this.connectionConfig = new ConnectionConfig(credential); this.authorisationConfig = new AuthorisationConfig(credential); this.connection = ConnectionFactory.getConnection(connectionConfig); } public boolean isConnected() { return connection.isConnected(); } public boolean isUnicode() { try { return connection.getServerInfo().isUnicodeEnabled(); } catch (Exception e) { return false; } } /** * Checks the Perforce server version number and returns true if greater * than or equal to the min version. The value of min must be of the form * 20092 or 20073 (corresponding to 2009.2 and 2007.3 respectively). * * @param min * @return */ public boolean checkVersion(int min) { int ver = connection.getServerVersionNumber(); return (ver >= min); } public boolean login() throws Exception { connection.setUserName(authorisationConfig.getUsername()); // CHARSET is not defined (only for client access) if (connection.getServerInfo().isUnicodeEnabled()) { connection.setCharsetName("utf8"); } // exit early if logged in if (isLogin()) return true; switch (authorisationConfig.getType()) { case PASSWORD: String pass = authorisationConfig.getPassword(); connection.login(pass); break; case TICKET: String ticket = authorisationConfig.getTicketValue(); connection.setAuthTicket(ticket); break; case TICKETPATH: String path = authorisationConfig.getTicketPath(); connection.setTicketsFilePath(path); break; default: throw new Exception("Unknown Authorisation type: " + authorisationConfig.getType()); } return isLogin(); } public void logout() throws Exception { if (isLogin()) { connection.logout(); } } private boolean isLogin() throws Exception { String status = connection.getLoginStatus(); if (status.contains("not necessary")) { return true; } if (status.contains("ticket expires in")) { return true; } // If there is a broker or something else that swallows the message if (status.isEmpty()) { return true; } logger.info("P4:login failed '" + status + "'"); return false; } public void setClient(Workspace workspace) throws Exception { if (connectionConfig.isUnicode()) { String charset = "utf8"; connection.setCharsetName(charset); } // Setup/Create workspace based on type iclient = workspace.setClient(connection, authorisationConfig.getUsername()); // Ensure root and host fields are set iclient.setRoot(workspace.getRootPath()); iclient.setHostName(workspace.getHostName()); // Set clobber on to ensure workspace is always good ClientOptions options = new ClientOptions(); options.setClobber(true); iclient.setOptions(options); // Save client spec iclient.update(); // Set active client for this connection connection.setCurrentClient(iclient); } public void deleteClient(Workspace workspace) throws Exception { if (connectionConfig.isUnicode()) { String charset = "utf8"; connection.setCharsetName(charset); } String name = workspace.getName(); connection.deleteClient(name, true); } /** * Test to see if workspace is at the latest revision. * * @throws Exception */ public boolean updateFiles() throws Exception { // build file revision spec List<IFileSpec> syncFiles; syncFiles = FileSpecBuilder.makeFileSpecList("//..."); // Sync revision to re-edit SyncOptions syncOpts = new SyncOptions(); syncOpts.setNoUpdate(true); List<IFileSpec> syncMsg = iclient.sync(syncFiles, syncOpts); for (IFileSpec fileSpec : syncMsg) { if (fileSpec.getOpStatus() != FileSpecOpStatus.VALID) { String msg = fileSpec.getStatusMessage(); if (msg.contains("file(s) up-to-date.")) { return false; } } } return true; } // TODO for Jenkins 'Recent Changes' Report feature public int lastChange() throws Exception { Map<String, Object>[] map = connection.execMapCmd("cstat", new String[] { "//..." }, null); logger.info(map.toString()); return 0; } /** * Sync files to workspace at the specified change. * * @param change * @throws Exception */ public boolean syncFiles(int change) throws Exception { boolean success = true; log("SCM Task: syncing files at change: " + change); // build file revision spec List<IFileSpec> files; String revisions = "//...@" + change; files = FileSpecBuilder.makeFileSpecList(revisions); // Sync revision to re-edit SyncOptions syncOpts = new SyncOptions(); log("... sync " + revisions); List<IFileSpec> syncMsg = iclient.sync(files, syncOpts); success &= validateFileSpecs(syncMsg, "file(s) up-to-date."); return success; } /** * Cleans up the Perforce workspace after a previous build. Removes all * pending and abandoned files (equivalent to 'p4 revert -w'). * * @throws Exception */ public boolean tidyWorkspace() throws Exception { boolean success = true; log("SCM Task: cleanup workspace: " + iclient.getName()); // relies on workspace view for scope. List<IFileSpec> files; files = FileSpecBuilder.makeFileSpecList("//..."); // revert all pending and shelved revisions RevertFilesOptions rOpts = new RevertFilesOptions(); log("... [list] = revert //..."); List<IFileSpec> list = iclient.revertFiles(files, rOpts); success &= validateFileSpecs(list, "not opened on this client"); // check for added files and remove... log("... rm [list] | ABANDONED"); for (IFileSpec file : list) { if (file.getAction() == FileAction.ABANDONED) { String path = depotToLocal(file); if (path != null) { File unlink = new File(path); unlink.delete(); } } } // check status - find all extra files ReconcileFilesOptions statusOpts = new ReconcileFilesOptions(); statusOpts.setOutsideAdd(true); statusOpts.setNoUpdate(true); statusOpts.setUseWildcards(true); log("... [list] = reconcile -n -a //..."); List<IFileSpec> extra = iclient.reconcileFiles(files, statusOpts); success &= validateFileSpecs(extra, "- no file(s) to reconcile", "instead of", "empty, assuming text"); // remove added files log("... rm [list]"); for (IFileSpec e : extra) { String path = depotToLocal(e); if (path != null) { File unlink = new File(path); unlink.delete(); } } // check status - find all missing or changed statusOpts = new ReconcileFilesOptions(); statusOpts.setNoUpdate(true); statusOpts.setUseWildcards(true); log("... [list] = reconcile -n //..."); List<IFileSpec> update = iclient.reconcileFiles(files, statusOpts); success &= validateFileSpecs(update, "also opened by", "no file(s) to reconcile", "must sync/resolve", "exclusive file already opened"); // force sync to update files only if "no file(s) to reconcile" is not // present. if (validateFileSpecs(update, true, "also opened by", "must sync/resolve", "exclusive file already opened")) { SyncOptions syncOpts = new SyncOptions(); syncOpts.setForceUpdate(true); log("... sync -f [list]"); List<IFileSpec> syncMsg = iclient.sync(update, syncOpts); success &= validateFileSpecs(syncMsg, "file(s) up-to-date.", "file does not exist"); } // force sync any files missed due to INFO messages e.g. exclusive files for (IFileSpec spec : update) { if (spec.getOpStatus() != FileSpecOpStatus.VALID) { String msg = spec.getStatusMessage(); if (msg.contains("exclusive file already opened")) { String rev = msg.substring(0, msg.indexOf(" - can't ")); List<IFileSpec> f = FileSpecBuilder.makeFileSpecList(rev); SyncOptions syncOpts = new SyncOptions(); syncOpts.setForceUpdate(true); log("... sync -f " + rev); List<IFileSpec> syncMsg = iclient.sync(f, syncOpts); success &= validateFileSpecs(syncMsg, "file(s) up-to-date.", "file does not exist"); } } } return success; } /** * Workaround for p4java bug. The 'setLocalSyntax(true)' option does not * provide local syntax, so I have to use 'p4 where' to translate through * the client view. * * @param fileSpec * @return * @throws Exception */ private String depotToLocal(IFileSpec fileSpec) throws Exception { String depotPath = fileSpec.getDepotPathString(); if (depotPath == null) { depotPath = fileSpec.getOriginalPathString(); } if (depotPath == null) { return null; } List<IFileSpec> dSpec = FileSpecBuilder.makeFileSpecList(depotPath); List<IFileSpec> lSpec = iclient.where(dSpec); String path = lSpec.get(0).getLocalPathString(); return path; } private void printFile(String rev) throws Exception { byte[] buf = new byte[1024 * 64]; List<IFileSpec> file = FileSpecBuilder.makeFileSpecList(rev); GetFileContentsOptions printOpts = new GetFileContentsOptions(); InputStream ins = connection.getFileContents(file, printOpts); String localPath = depotToLocal(file.get(0)); File target = new File(localPath); if (target.exists()) { target.setWritable(true); } FileOutputStream outs = new FileOutputStream(target); BufferedOutputStream bouts = new BufferedOutputStream(outs); int len; while ((len = ins.read(buf)) > 0) { bouts.write(buf, 0, len); } ins.close(); bouts.close(); } /** * Unshelve review into workspace. Workspace is sync'ed to head first then * review unshelved. * * @param review * @throws Exception */ public boolean unshelveFiles(int review) throws Exception { boolean success = true; log("SCM Task: unshelve review: " + review); // build file revision spec List<IFileSpec> files; files = FileSpecBuilder.makeFileSpecList("//..."); // Sync workspace to head SyncOptions syncOpts = new SyncOptions(); log("... sync //..."); List<IFileSpec> syncMsg = iclient.sync(files, syncOpts); success &= validateFileSpecs(syncMsg, "file(s) up-to-date."); // Unshelve change for review List<IFileSpec> shelveMsg; log("... unshelve -f -s " + review + " //..."); shelveMsg = iclient.unshelveChangelist(review, files, 0, true, false); success &= validateFileSpecs(shelveMsg, "also opened by", "no such file(s)", "exclusive file already opened"); // force sync any files missed due to INFO messages e.g. exclusive files for (IFileSpec spec : shelveMsg) { if (spec.getOpStatus() != FileSpecOpStatus.VALID) { String msg = spec.getStatusMessage(); if (msg.contains("exclusive file already opened")) { String rev = msg.substring(0, msg.indexOf(" - can't ")); printFile(rev); log("... print " + rev); } } } // Remove opened files from have list. RevertFilesOptions rOpts = new RevertFilesOptions(); rOpts.setNoUpdate(true); log("... revert -k //..."); List<IFileSpec> rvtMsg = iclient.revertFiles(files, rOpts); success &= validateFileSpecs(rvtMsg, "file(s) not opened on this client"); return success; } /** * Get the change number for the last change within the scope of the * workspace view. * * @return * @throws Exception */ public int getHead() throws Exception { // build file revision spec List<IFileSpec> files; files = FileSpecBuilder.makeFileSpecList("//..."); GetChangelistsOptions opts = new GetChangelistsOptions(); opts.setMaxMostRecent(1); List<IChangelistSummary> list = connection.getChangelists(files, opts); int change = 0; if (list.get(0) != null) { change = list.get(0).getId(); } return change; } public Changelist getChange(int id) throws Exception { return (Changelist) connection.getChangelist(id); } /** * Disconnect from the Perforce Server. * * @throws Exception */ public void disconnect() throws Exception { connection.disconnect(); } /** * Look for a message in the returned FileSpec from operation. * * @param fileSpecs * @param ignore * @return * @throws ConverterException */ public boolean validateFileSpecs(List<IFileSpec> fileSpecs, String... ignore) throws Exception { return validateFileSpecs(fileSpecs, false, ignore); } public boolean validateFileSpecs(List<IFileSpec> fileSpecs, boolean quiet, String... ignore) throws Exception { for (IFileSpec fileSpec : fileSpecs) { if (fileSpec.getOpStatus() != FileSpecOpStatus.VALID) { String msg = fileSpec.getStatusMessage(); // superfluous p4java message boolean unknownMsg = true; ArrayList<String> ignoreList = new ArrayList<String>(); ignoreList.addAll(Arrays.asList(ignore)); for (String istring : ignoreList) { if (msg.contains(istring)) { // its a known message unknownMsg = false; } } // check and report unknown message if (unknownMsg) { if (!quiet) { log("P4JAVA: " + msg); logger.warning("p4java: " + msg); } return false; } } } return true; } /** * Finds a Perforce Credential based on the String id. * * @return a P4StandardCredentials credential or null if not found. */ private static P4StandardCredentials findCredential(String id) { Class<P4StandardCredentials> type = P4StandardCredentials.class; Jenkins scope = Jenkins.getInstance(); Authentication acl = ACL.SYSTEM; DomainRequirement domain = new DomainRequirement(); List<P4StandardCredentials> list; list = CredentialsProvider.lookupCredentials(type, scope, acl, domain); for (P4StandardCredentials c : list) { if (c.getId().equals(id)) { return c; } } return null; } public void log(String msg) { if (listener == null) { return; } listener.getLogger().println(msg); } public void stop() throws Exception { connection.execMapCmd("admin", new String[] { "stop" }, null); } }
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#36 | 9672 | Paul Allen | Refactor name from 'p4_client' to 'p4'. | ||
#35 | 9460 | Paul Allen | Skip login, if no password set. | ||
#34 | 9447 | Paul Allen |
Small mod to force a login for each connection. There may be a case with a broker/replica when the login test may incorrectly report the users is logged. |
||
#33 | 9429 | Paul Allen |
Fix SCM Polling bug. Client workspace was not set (unless by an earlier build). - Move listChanges method to ClientHelper. |
||
#32 | 9091 | Paul Allen |
Added Changelist build filtering for SCM polling: - Configuration uses 'repeatableHeteroProperty' - Filter on Perforce username - Filter on Perforce Depot path (no wildcard support) |
||
#31 | 9077 | Paul Allen |
Added support for automatic Labels as a post-build Action. Ported original code for promoted builds, but not tested. |
||
#30 | 9055 | Paul Allen |
Label support. Build at a label using the pram 'label'. This includes adding the label to the ChangeEntry, building the change reports and Browser links to Swarm. (TPI-102) |
||
#29 | 8969 | Paul Allen |
Adds all contributing change-lists for the build to the change log (using p4 cstat). - Includes exception logging for server connection to the Jenkins console. |
||
#28 | 8940 | Paul Allen |
Major refactor for the ConnectionHelper class to simplify serialisation. Fixed remote Jenkins JNLP slave connection issue. ClientHelper now extends ConnectionHelper and takes on all methods that require a client workspace. P4StandardCredentials is sent to the remote node instead of Credentials ID due to an issue accessing the Credentials store over a remote connection. For simplicity Client ID (workspace name) is serialised instead of the Workspace object. |
||
#27 | 8924 | Paul Allen | Sync workspace to last change in workspace (not depot). | ||
#26 | 8915 | Paul Allen |
Support for ChangeLog and RepoBrowser. - Added RepoBrowser for Swarm (porting the others should be easy) - ChangeLog XML file now only stores the changelist number all other information is fetched from Perforce |
||
#25 | 8909 | Paul Allen |
[TPI-83] Clean Workspace that may contain files with wildcards. Added (-f) flag to reconcile and ignore warnings for empty files. |
||
#24 | 8899 | Paul Allen | Move Workspace setup/creation code to Implementation class. | ||
#23 | 8864 | Paul Allen | BugFix on template workspaces; view was not generated during creation. | ||
#22 | 8852 | Paul Allen |
Functional test for unshelve. Includes fix for reconcile and unshelve exclusive locked files. |
||
#21 | 8815 | Paul Allen | P4D test harness. | ||
#20 | 8805 | Paul Allen | Minor fix to ignore Perforce encoding messages when finding 'extra' files during a 'reconcile -a'. | ||
#19 | 8771 | Paul Allen |
Perforce Server 12.1 min check for: Build configuration and password/ticket credentials. Includes: - Added logging for Perforce connections (fine) and set connection pool to 2. - Add 'none' to empty charset list (connection bug?) - Supress P4Java errors for syncing ubinary, xtext, unicode |
||
#18 | 8770 | Paul Allen |
Implemented SCM hook for when Jenkins deletes a workspace. The PerforceScm plugin deletes the workspace, but leaves Jenkins to clean up the local files and directories. (includes dummy 'p4 cstat' code for change reporting) |
||
#17 | 8766 | Paul Allen |
Bug fix: Client Owner not set when creating workspace. Now jenkins will set the owner for new workspaces and take ownership of other manual workspaces it uses for build. |
||
#16 | 8764 | Paul Allen | Supress P4Java warnings, to prevent SCM tasks from failing. | ||
#15 | 8763 | Paul Allen | More Console Output logging | ||
#14 | 8762 | Paul Allen |
Console Ouptut logging for SCM build steps. - Removed SLF4J and used old style logger (matching Jenkins) - Set Client Host filed to null, Jenkins sometimes gives an IP address. - Test p4java setps in SCM tasks for success(true/false) |
||
#13 | 8756 | Paul Allen | Added Stream field to Manual Workspace config. | ||
#12 | 8749 | Paul Allen |
Split off Worksapce Spec from Manual Workspace configuration into a Property Jelly item. Plan to autoload values based on selected client. |
||
#11 | 8744 | Paul Allen |
Support basic SCM Polling. When Jenkins has "Poll SCM" checked the Perforce SCM provider just runs a 'p4 sync -n' on the Workspace. Simple and Fast. |
||
#10 | 8741 | Paul Allen |
Added "Test Connection" to Perforce Password and Ticket Credentials. Does not support SSL connections...yet |
||
#9 | 8738 | Paul Allen |
Workspace Name Formatter. For Template and Stream workspaces it allows the substitution of the following tags: ${node} The name given to the slave Jenkins node. ${hostname} The hostname for the slave Jenkins node. ${project} The name of the Jenkins build Job. ${hash} Unique hash code of the Jenkins node. |
||
#8 | 8725 | Paul Allen |
Minor fixes: - Improved tidy workspace method - Fix for template clients - After an unshelf revert -k files - Minor bug in CheckoutTask - Clear callback urls to prevet reuse |
||
#7 | 8697 | Paul Allen |
Added Manual workspace option for user to define Options/LineEnd/View etc... in Jenkins. |
||
#6 | 8694 | Paul Allen | Added support for unshelve and revert -w behaviour for builds. | ||
#5 | 8664 | Paul Allen | Simplified connection to Perforce to get around the SCM initilisation (or lack of). | ||
#4 | 8662 | Paul Allen | Added auto fill and checks for streams and templates. | ||
#3 | 8661 | Paul Allen | Workspace auto fill | ||
#2 | 8641 | Paul Allen | Added workspace helper (setClient) and template/stream types. | ||
#1 | 8629 | Paul Allen |
Added p4java with connection/authorisation helper classes. Included SSL support and detection of Unicode servers. |