/* * P4.Net * Copyright (c) 2006 Shawn Hladky Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ using System; using System.Text; using p4dn; using System.Diagnostics; using P4API.Exceptions; namespace P4API { /// <summary> /// Delegate to handle the OnPrompt event. /// </summary> /// <param name="sender">Sender</param> /// <param name="args">P4PromptEventArgs</param> /// <remarks>Handle Perforce "prompts". Prompts are from commands that ask the user to respond, such as: /// <list> /// <li>login</li> /// <li>passwd</li> /// <li>resolve (without an -a* switch)</li> /// </list> /// </remarks> /// <seealso cref="OnPrompt"/> public delegate void OnPromptEventHandler(object sender, P4PromptEventArgs args); /// <summary> /// A connection to a Perforce server instance. /// </summary> /// <include file='CodeDocuments.xml' path='//P4Connection/remarks' /> public class P4Connection : IDisposable { private ClientApi m_ClientApi; private bool _tagged = true; private bool _Initialized; private string _CallingProgram; private string _CallingProgramVersion; private string _Client = null; private string _Port = null; private string _User = null; private string _Host = null; private string _CWD = null; private string _Charset = null; private DateTime _p4epoch = new DateTime(1970, 1, 1); private TimeSpan _offset; private bool _timespanSet = false; private P4ExceptionLevels _exceptionLevel = P4ExceptionLevels.NoExceptionOnWarnings ; /// <summary> /// Raised when Perforce is prompting for a response. /// </summary> /// <remarks>Handle Perforce "prompts". Prompts are from commands that ask the user to respond, such as: /// <list> /// <li>login</li> /// <li>passwd</li> /// <li>resolve (without an -a* switch)</li> /// </list> /// </remarks> /// <include file='CodeDocuments.xml' path='//OnPrompt/example' /> public event OnPromptEventHandler OnPrompt; private ClientApi _ClientAPI { get { if (m_ClientApi == null) { m_ClientApi = new ClientApi(); } //if (!_Initialized) throw new Exceptions.ServerNotConnected(); return m_ClientApi; } //set { m_ClientApi = value; } } /// <summary> /// Initializes a new instance of the <see cref="T:P4Connection"/> class. /// </summary> public P4Connection() { this._Initialized = false; this._CallingProgram = "P4.Net API"; this._CallingProgramVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(); } private TimeSpan _TimeOffset { get { if (!_timespanSet) { try { P4Record info = Run("info")[0]; string offset = info.Fields["serverDate"]; offset = offset.Split(' ')[2]; int sepPosition = offset.Length - 2; int offsetMinutes = int.Parse(offset.Substring(sepPosition)); int offsetHours = int.Parse(offset.Substring(0,sepPosition)); _offset = new TimeSpan(offsetHours, offsetMinutes, 0); _timespanSet = true; } catch { throw; } } return _offset; } } /// <summary> /// Gets/Sets the client workspace. /// </summary> /// <remarks>Many Perforce commands require a valid client spec to be set in order to run.</remarks> public string Client { get { return _ClientAPI.Client; } set { _Client = value; _ClientAPI.SetClient(value); } } /// <summary> /// Gets/Sets the current working directory. /// </summary> /// <remarks>Setting CWD can be used so that relative paths are specified. Keep in mind that changing /// the CWD also might cause P4CONFIG settings to change.</remarks> public string CWD { get { return _ClientAPI.Cwd; } set { _CWD = value; _ClientAPI.SetCwd(value); } } /// <summary> /// Gets/Sets the Exception level when running Perforce commands. /// </summary> /// <remarks><para>This property controls when P4.Net will throw exceptions /// when the underlying Perforce commands raise errors and warnings.</para> /// <para>The default is <paramref name="P4ExceptionLevels.NoExceptionOnWarnings"/></para></remarks> public P4ExceptionLevels ExceptionLevel { get { return _exceptionLevel; } set { _exceptionLevel = value; } } /// <summary> /// Converts Perforce date (integer) to .Net DateTime. /// </summary> /// <remarks>The Perforce API returns most dates as an integer representing the number of seconds /// since 1/1/1970. It is in UTC. The command line client generally returns dates as strings /// in the Perforce server's time zone. The ConvertDate methods use the server's time zone for /// the offset, not necessarily the local time zone.</remarks> /// <returns>DateTime in .Net format.</returns> public DateTime ConvertDate(int p4Date) { DateTime utc = _p4epoch.AddSeconds(p4Date); return utc.Add(_TimeOffset); } /// <summary> /// Converts Perforce date (integer value as a string) to .Net DateTime. /// </summary> /// <remarks>The Perforce API returns most dates as an integer representing the number of seconds /// since 1/1/1970. It is in UTC. The command line client generally returns dates as strings /// in the Perforce server's time zone. The ConvertDate methods use the server's time zone for /// the offset, not necessarily the local time zone.</remarks> public DateTime ConvertDate(string p4Date) { return ConvertDate(int.Parse(p4Date)); } /// <summary> /// Converts .Net DateTime to Perforce date (integer). /// </summary> /// <remarks>The Perforce API returns most dates as an integer representing the number of seconds /// since 1/1/1970. It is in UTC. The command line client generally returns dates as strings /// in the Perforce server's time zone. The ConvertDate methods use the server's time zone for /// the offset, not necessarily the local time zone.</remarks> public int ConvertDate(DateTime date) { TimeSpan ts = date.Subtract(_p4epoch); ts = ts.Subtract(_TimeOffset); return ts.Days*24*60*60 + ts.Hours*60*60 + ts.Minutes*60 + ts.Seconds; } /// <summary> /// Gets/Sets the Host-name of the client. /// </summary> /// <remarks>Forcing the Host to a different name is useful when using a client that /// was defined on a different host. Of course, that can cause issues with the client's /// have list, so use with caution.</remarks> public string Host { get { return _ClientAPI.Host; } set { _Host = value; _ClientAPI.SetHost(value); } } /// <summary> /// Gets/Sets the Perforce Server port. /// </summary> /// <value>The port.</value> public string Port { get { return _ClientAPI.Port; } set { if (_Initialized) throw new ServerAlreadyConnected(); _Port = value; _ClientAPI.SetPort(value); } } /// <summary> /// Gets/Sets the User login used to connect to Perforce. /// </summary> /// <value>The user.</value> public string User { get { return _ClientAPI.User; } set { _User = value; _ClientAPI.SetUser(value); } } /// <summary> /// Gets/Sets the client character set. /// </summary> /// <remarks>Internationalized servers are not yet fully supported!</remarks> /// <value>The client's charset.</value> public string Charset { get { return _ClientAPI.Charset; } set { _Charset = value; _ClientAPI.SetCharset(value); } } /// <summary> /// Get/Sets the name of the calling program. /// </summary> /// <remarks>This value is seen in p4 monitor and in the server log files.</remarks> /// <value>Defaults to "P4.Net API"</value> public string CallingProgram { get { return _CallingProgram; } set { if (_Initialized) throw new ServerNotConnected_SetVar_AfterInit(); _CallingProgram = value; // Set these in the initialize section } } /// <summary> /// Gets/Sets the version of the calling program's version. /// </summary> /// <remarks>This value is seen in p4 monitor and the server log files.</remarks> /// <value>Defaults to the assembly version of the P4API.dll.</value> public string CallingVersion { get { return _CallingProgramVersion; } set { if (_Initialized) throw new ServerNotConnected_SetVar_AfterInit(); _CallingProgramVersion = value; // Set these in the initialize section } } /// <summary> /// Sets the password to conenct with. /// </summary> /// <remarks>Do not set this value with a server running security level 2 or higher. Use the Login() method.</remarks> public string Password { set { _ClientAPI.SetPassword(value); } } /// <summary> /// Sets the ticket file used for Authentication. /// </summary> public string TicketFile { set { // _ClientAPI.SetTicketFile(value); } } /// <summary> /// Connect to the Perforce server /// </summary> public void Connect() { EstablishConnection(_tagged); } /// <summary> /// Disconnect from the Perforce Server /// </summary> public void Disconnect() { CloseConnection(); } /// <summary> /// Login to the Perforce Server /// </summary> /// <param name="password">The password.</param> public void Login(string password) { P4ExceptionLevels oldLevel = _exceptionLevel; try { P4UnParsedRecordSet r = new P4UnParsedRecordSet(); r.LoginPassword = password; EstablishConnection(false); string[] Args = { }; _exceptionLevel = P4ExceptionLevels.NoExceptionOnErrors; RunIt("login", Args, r.ResultClientUser); //for good measure delete the password r.LoginPassword = null; if (r.HasErrors()) { throw new P4API.Exceptions.InvalidLogin(r.ErrorMessage); } } finally { _exceptionLevel = oldLevel; } } /// <summary> /// Creates a new pending changelist. /// </summary> /// <param name="Description">The description.</param> /// <returns>P4PendingChangelist object representing the named pending changelist</returns> public P4PendingChangelist CreatePendingChangelist(string Description) { EstablishConnection(false); return new P4PendingChangelist(Description, this); } /// <summary> /// Fetch a form object from Perforce. /// </summary> /// <param name="FormCommand">The form command.</param> /// <param name="Args">The args.</param> /// <returns> /// A Form object. The fields of the form can be read or updated. If you update a filed, you can save it with Save_Form. /// </returns> /// <include file='CodeDocuments.xml' path='//Forms/remarks' /> public P4Form Fetch_Form(string FormCommand, params string[] Args) { P4FormRecordSet r = new P4FormRecordSet(FormCommand); EstablishConnection(true); string[] AllArgs = new string[Args.Length + 1]; AllArgs[0] = "-o"; for (int i = 0; i < Args.Length ; i++) { if (Args[i] == "-o") { throw new InvalidFormArgument(); } AllArgs[i+1] = Args[i]; } RunIt(FormCommand, AllArgs, r.ResultClientUser); // The Fetch_Form command must always throw an error // because there will be no form to return if there's a // problem if (r.HasErrors()) { throw new FormFetchException(FormCommand, r.ErrorMessage); } return r.Form; } /// <summary> /// Saves the form to Perforce. /// </summary> /// <param name="Form">The P4Form object retrieved from Fetch_Form.</param> /// <include file='CodeDocuments.xml' path='//Forms/remarks' /> /// <returns>P4UnParsedRecordSet. Output can be parsed to verify the form was processed correctly.</returns> public P4UnParsedRecordSet Save_Form(P4Form Form) { return Save_Form(Form, false); } /// <summary> /// Saves the form to Perforce. /// </summary> /// <param name="Form">The P4Form object retrieved from Fetch_Form.</param> /// <param name="Force">True to pass the '-f' flag when saving.</param> /// <include file='CodeDocuments.xml' path='//Forms/remarks' /> /// <returns>P4UnParsedRecordSet. Output can be parsed to verify the form was processed correctly.</returns> public P4UnParsedRecordSet Save_Form(P4Form Form, bool Force) { P4UnParsedRecordSet r = new P4UnParsedRecordSet(); r.SpecDef = Form.SpecDef; r.FormInput = Form.AllFieldDictionary ; EstablishConnection(true); if (Force) { string[] Args = { "-i", "-f" }; RunIt(Form.FormCommand, Args, r.ResultClientUser, true); } else { string[] Args = { "-i"}; RunIt(Form.FormCommand, Args, r.ResultClientUser, true); } if ((( _exceptionLevel == P4ExceptionLevels.ExceptionOnBothErrorsAndWarnings || _exceptionLevel == P4ExceptionLevels.NoExceptionOnWarnings) && r.HasErrors()) || ( _exceptionLevel == P4ExceptionLevels.ExceptionOnBothErrorsAndWarnings && r.HasWarnings()) ) { throw new RunUnParsedException(r); } return r; } /// <summary> /// Determines if the Perforce connection is valid. /// </summary> /// <param name="checkLogin">if set to <c>true</c> it will check if the user is logged in.</param> /// <param name="checkClient">if set to <c>true</c> it will verify the client exists.</param> /// <returns> /// <c>true</c> if the connection is valid; otherwise, <c>false</c>. /// </returns> /// <remarks> /// IsValidConnection performs the following 3 checks (depending on the arguments passed). /// <list type="numbered"> /// <li>Runs p4 info, and verifies the server is valid.</li> /// <li>If checkClient is true, verifies that p4 info returns a clientName that is not "*unknown*.</li> /// <li>If checkLogin is true, verifies that p4 login -s does not have errors.</li> /// </list> /// </remarks> public bool IsValidConnection(bool checkLogin, bool checkClient) { P4ExceptionLevels oldExLevel = this.ExceptionLevel; try { this.ExceptionLevel = P4ExceptionLevels.NoExceptionOnErrors; P4RecordSet r = Run("info"); if (r.HasErrors()) return false; if (r.Records.Length != 1) return false; if (checkClient) { if (!r.Records[0].Fields.ContainsKey("clientName")) return false; if (r.Records[0].Fields["clientName"] == "*unknown*") return false; } if (checkLogin) { P4UnParsedRecordSet ur = RunUnParsed("login", "-s"); if (ur.HasErrors()) return false; } } catch { // something went way wrong return false; } finally { // set the ExceptionLevel back this.ExceptionLevel = oldExLevel; } return true; } /// <summary> /// Executes a Perforce command in tagged mode. /// </summary> /// <param name="Command">The command.</param> /// <param name="Args">The arguments to the Perforce command. Remember to use a dash (-) in front of all switches</param> /// <returns>A P4Recordset containing the results of the command.</returns> public P4RecordSet Run(string Command, params string[] Args) { P4RecordSet r = new P4RecordSet(); EstablishConnection(true); RunIt(Command, Args, r.ResultClientUser); if ((( _exceptionLevel == P4ExceptionLevels.ExceptionOnBothErrorsAndWarnings || _exceptionLevel == P4ExceptionLevels.NoExceptionOnWarnings) && r.HasErrors()) || ( _exceptionLevel == P4ExceptionLevels.ExceptionOnBothErrorsAndWarnings && r.HasWarnings() ) ) { throw new RunException(r); } return r; } /// <summary> /// Executes a Perforce command in non-tagged mode. /// </summary> /// <param name="Command">The command.</param> /// <param name="Args">The args.</param> /// <returns></returns> public P4UnParsedRecordSet RunUnParsed(string Command, params string[] Args) { P4UnParsedRecordSet r = new P4UnParsedRecordSet(); P4BaseRecordSet.OnPromptEventHandler handler = new P4BaseRecordSet.OnPromptEventHandler(this.HandleOnPrompt); r.OnPrompt += handler; EstablishConnection(false); RunIt(Command, Args, r.ResultClientUser); r.OnPrompt -= handler; if ((( _exceptionLevel == P4ExceptionLevels.ExceptionOnBothErrorsAndWarnings || _exceptionLevel == P4ExceptionLevels.NoExceptionOnWarnings) && r.HasErrors()) || ( _exceptionLevel == P4ExceptionLevels.ExceptionOnBothErrorsAndWarnings && r.HasWarnings() ) ) { throw new RunUnParsedException(r); } return r; } private void HandleOnPrompt(object sender, P4PromptEventArgs e) { e.Response = RaiseOnPromptEvent(e.Message); } #region Private Helper Methods private void EstablishConnection(bool tagged) { // If we're switching contexts from tagged to un-tagged, // then we need to reset the connection if ((m_ClientApi != null) && tagged != _tagged) { CloseConnection(); } if (m_ClientApi == null) { m_ClientApi = new ClientApi(); } if (m_ClientApi.Dropped()!=0) { //Console.WriteLine("Dropped!"); CloseConnection(); } if (!_Initialized) { Error err = null; try { _tagged = tagged; err = m_ClientApi.CreateError(); if (tagged) m_ClientApi.SetProtocol("tag", ""); if (tagged) m_ClientApi.SetProtocol("specstring", ""); //May have lost our settings... reset here if (_Client != null) m_ClientApi.SetClient(_Client); if (_User != null) m_ClientApi.SetUser(_User); if (_CWD != null) m_ClientApi.SetCwd(_CWD); if (_Charset != null) m_ClientApi.SetCharset(_Charset); if (_Host != null) m_ClientApi.SetHost(_Host); if (_Port != null) m_ClientApi.SetPort(_Port); m_ClientApi.Init(err); if (err.IsFatal()) { //Trace.WriteLine(err.Fmt()); //Trace.WriteLine("Unable to connect to Perforce!"); throw new Exception("Unable to connect to Perforce!"); } if (_CallingProgram != null) m_ClientApi.SetProg(_CallingProgram); if (_CallingProgramVersion != null) m_ClientApi.SetVersion(_CallingProgramVersion); _Initialized = true; err.Dispose(); } catch (Exception e) { //Trace.WriteLine(e.Message); m_ClientApi.Final(err); err.Dispose(); m_ClientApi.Dispose(); m_ClientApi = null; throw new PerforceInitializationError(e.Message); } } } private void CloseConnection() { // Need to reset the connection if (_Initialized) { Error err = m_ClientApi.CreateError(); m_ClientApi.Final(err); err.Dispose(); } if (m_ClientApi != null) { m_ClientApi.Dispose(); m_ClientApi = null; } _Initialized = false; } private void RunIt(string command, string[] args, ClientUser cu) { RunIt(command, args, cu, false ); } private void RunIt(string command, string[] args, ClientUser cu, bool SaveForm) { m_ClientApi.SetArgv(args); if (SaveForm) { m_ClientApi.SaveForm(command, cu); } else { m_ClientApi.Run(command, cu); } } // Wrap event invocations inside a protected virtual method // to allow derived classes to override the event invocation behavior private string RaiseOnPromptEvent(string Message) { // Make a temporary copy of the event to avoid possibility of // a race condition if the last subscriber unsubscribes // immediately after the null check and before the event is raised. OnPromptEventHandler handler = OnPrompt; // Event will be null if there are no subscribers if (handler != null) { P4PromptEventArgs e = new P4PromptEventArgs(Message); // Use the () operator to raise the event. handler(this, e); return e.Response; } else { return string.Empty; } } #endregion #region IDisposable Members /// <summary> /// Calls Disconnect. /// </summary> public void Dispose() { this.Disconnect(); } #endregion } }
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#20 | 7709 | Shawn Hladky | P4.Net: Cleanup, documentation, and a a start for implementing an object-based output for filelog. | ||
#19 | 6505 | Shawn Hladky |
P4.Net: Multiple Changes 1. Update samples to VS2008 and new bin paths 2. Update MSBuild sync tasks to have IgnoredWarnings parameter 3. Added public class for P4RecordsetCallback. This allows consumers to easily migrate code that uses Recordsets to also take advantage of callback hooks. 4. Reworked method signiture of RunCallback. Removed tagged parameter and added RunCallbackUnparsed method. Made Callback parameter first so command and arguments are next to one-another. Note: this is a BREAKING CHANGE if you are using callbacks. 5. Reworked so switching between tagged and untagged runs will not disconnect/reconnect. 6. Add initial work for a file diffing object. |
||
#18 | 6472 | Shawn Hladky | P4.Net: Build script updates to support x64 | ||
#17 | 6457 | Shawn Hladky |
P4.Net: Added form_save overload to allow arbitrary arguments (primarily for -u flag on submitted changelists) Added unit tests for Resolve workflow. Still need work on this and test partially fails. Updated some internal data structures to use generics Added documentation comments |
||
#16 | 6386 | Shawn Hladky | P4.Net: Optimized connections by not reconnecting between tagged/untagged transitions. | ||
#15 | 6353 | Shawn Hladky |
P4.Net: Implemented diff functionality Implemented Merge functionality Converted print commands to the callback interface |
||
#14 | 6243 | Shawn Hladky | P4.Net: Change Callback from interface to abstract class | ||
#13 | 6238 | Shawn Hladky |
P4.Net: More work on callback interface. Run and RunUnparsed now use the callback interface under the covers. |
||
#12 | 6223 | Shawn Hladky |
P4.Net. Fixing bug when running Fetch_Form and Connect() is has not been called. |
||
#11 | 6182 | Shawn Hladky |
P4.Net. First stab at a callback interface for real-time handling of perforce output. Upgrade to VS 2008. |
||
#10 | 6102 | Shawn Hladky |
P4.Net: Documentation. Fixed SetTicketFile bug. Added form processing with SpecDef. Stubbed-out code for launching external merge tool, but is disabled since it's too difficult to use. |
||
#9 | 6052 | Shawn Hladky |
P4.Net: allow build against 2007.2 Code documentation |
||
#8 | 5978 | Shawn Hladky |
P4.Net. Unit tests for diff output. Added properties for server and client api level |
||
#7 | 5915 | Shawn Hladky |
p4.net Fixed diff2 bug. Re-worked array field logic More unit tests Organized P4Connection w/ #region blocks, and moved methods around |
||
#6 | 5902 | Shawn Hladky |
P4.net. More unit tests and bug fixes |
||
#5 | 5878 | Shawn Hladky |
P4.Net: 1.0, support for raw spec processing. Update copyright. Fix build script. Bugs found along the way. |
||
#4 | 5874 | Shawn Hladky |
P4.Net: v1.0 WIP. Upgrade API to 2006.2. Fixes/tests for print and unicode servers. Changed ConvertDate to use client's timezone... old logic trying to use server's timezone didn't account for daylight savings time. |
||
#3 | 5842 | Shawn Hladky |
P4.Net Update test harness for unicode server. Add methods to support p4 print. |
||
#2 | 5838 | Shawn Hladky | P4.Net: WIP support for unicode server | ||
#1 | 5830 | Shawn Hladky | P4.Net: reorg to support release branches | ||
//guest/shawn_hladky/P4.Net/src/P4API/P4Connection.cs | |||||
#17 | 5812 | Shawn Hladky | P4.Net: More documentation. | ||
#16 | 5801 | Shawn Hladky |
P4.Net. More misc changes |
||
#15 | 5798 | Shawn Hladky |
P4.Net... still not ready for beta Added license to all files Added several doc files Misc bugs |
||
#14 | 5734 | Shawn Hladky | More unit tests, and bug fixes. | ||
#13 | 5678 | Shawn Hladky |
WIP... more tests. OnPrompt event to recieve input from prompts. |
||
#12 | 5636 | Shawn Hladky |
1. Added test harness framework, and some initial tests 2. Fixed many bugs (oddly enough identified by the unit tests) 3. Fixes so will build 1.1 Framework (and build batch files actually work) 4. Pathetic attempt at documentation |
||
#11 | 5447 | Shawn Hladky | refactor, and added options class | ||
#10 | 5438 | Shawn Hladky | refactoring, and documentation code | ||
#9 | 5436 | Shawn Hladky | P4.Net -- Added some high-level functionality | ||
#8 | 5433 | Shawn Hladky | P4.Net More refactoring | ||
#7 | 5431 | Shawn Hladky |
Refactoring... step 1. |
||
#6 | 5427 | Shawn Hladky | P4.Net -- several fixes and added sample application | ||
#5 | 5411 | Shawn Hladky | WIP -- forms are working now. | ||
#4 | 5373 | Shawn Hladky | P4.Net: Still WIP, but some things starting to work | ||
#3 | 5362 | Shawn Hladky | Chipping away at the API changes | ||
#2 | 5350 | Shawn Hladky | WIP few more changes | ||
#1 | 5349 | Shawn Hladky |
Initial check-in for the new API interface. Nothing works yet, but it should compile at least. |